Skip to content

Commit 980944c

Browse files
committed
Forward Gemini finishReason to usage and correct truncation docs
1 parent a08d129 commit 980944c

5 files changed

Lines changed: 75 additions & 9 deletions

File tree

vendor/intx-inference/PATCHES.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -414,10 +414,15 @@ prior code fell back to a `{ _raw: <partial JSON> }` tool_call block, which
414414
the reactor dispatched — executing a tool with truncated arguments when the
415415
model was cut off by `max_tokens` (CL-7783: a truncated `Bash`
416416
`rm -rf /tm…` fragment reached the executor). Now: when `stopReason` is
417-
`max_tokens` and a tool call is still open, the turn fails retryably with a
418-
message naming the tool and the truncated prefix, telling the model to retry
419-
with a narrower scope; any other unparseable-args case fails retryably as
420-
invalid JSON. Two supporting changes: `providers/anthropic.ts` parses
417+
`max_tokens` and a tool call is still open, the turn fails with an
418+
`inference.error` naming the tool and the truncated prefix, advising a
419+
larger budget or a narrower scope for the model's next attempt; any other
420+
unparseable-args case fails the same way as invalid JSON. Both errors carry
421+
category `retryable`, but end-of-stream finalization always runs after the
422+
attempt has committed visible output, so the harness commitment boundary
423+
suppresses the mechanical retry — the failure is terminal for the turn, and
424+
the message is guidance for the next attempt rather than a re-issued retry.
425+
Two supporting changes: `providers/anthropic.ts` parses
421426
`stop_reason` out of `MessageDelta` (previously stripped by the schema) and
422427
surfaces it on `inference.usage`, and `vendor/intx-types`' `InferenceUsageEvent`
423428
gains the optional `stopReason` field both halves flow through. Guarded by the
@@ -426,7 +431,12 @@ exact incident wire sequence and asserts no `tool_call` block reaches the
426431
reactor. The OpenAI-compatible adapter was audited for the same path: it has
427432
no adapter-local args fallback (the harness was the only dispatch site) but
428433
still drops `finish_reason` on both paths, so OpenAI streams get the generic
429-
invalid-JSON failure rather than the truncation-specific message.
434+
invalid-JSON failure rather than the truncation-specific message. The Gemini
435+
adapter forwards its terminal `finishReason` onto `inference.usage` (same
436+
spread idiom as Anthropic), but forwards the provider's raw spelling
437+
(`MAX_TOKENS`), which the harness `max_tokens` comparison does not match —
438+
so Gemini truncation still lands on the generic invalid-JSON failure until
439+
the harness normalizes provider spellings.
430440

431441
**Disposition:** Promotion candidate. Safety/correctness fix — prevents
432442
executing tools with truncated arguments after a `max_tokens` cutoff.

vendor/intx-inference/src/harness.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,8 +1124,10 @@ async function* runSingleAttempt(
11241124
// validate every open call before emitting any of them, and never
11251125
// dispatch a call whose arguments are incomplete or unparseable. A turn
11261126
// cut at max_tokens with calls still open is unambiguous truncation;
1127-
// anything else unparseable is still not a normal call. Both fail the
1128-
// turn retryably so the model can re-issue it with room to finish.
1127+
// anything else unparseable is still not a normal call. Both yield an
1128+
// inference.error (category retryable) naming the call; post-commit the
1129+
// harness surfaces it terminally rather than mechanically retrying, so
1130+
// the message guides the model's next attempt.
11291131
const finalizedToolCalls: {
11301132
tc: ToolCallState;
11311133
parsedArgs: Record<string, unknown>;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { describe, expect, test } from "bun:test";
2+
import type { LastCycleSource } from "@intx/types/runtime";
3+
import { createGoogleGenAIAdapter } from "./google-genai";
4+
5+
const TEST_SOURCE: LastCycleSource = {
6+
sourceId: "test-google-genai",
7+
provider: "google-genai",
8+
model: "test-gemini-model",
9+
};
10+
11+
describe("google-genai adapter — finishReason forwarding (CL-7783)", () => {
12+
test("terminal finishReason surfaces on the usage event", () => {
13+
const adapter = createGoogleGenAIAdapter(TEST_SOURCE);
14+
const events = adapter.parseResponse(
15+
JSON.stringify({
16+
candidates: [
17+
{
18+
content: { parts: [{ text: "partial" }], role: "model" },
19+
finishReason: "MAX_TOKENS",
20+
index: 0,
21+
},
22+
],
23+
usageMetadata: { promptTokenCount: 5, candidatesTokenCount: 3 },
24+
}),
25+
);
26+
const usage = events.filter((e) => e.type === "inference.usage");
27+
expect(usage).toHaveLength(1);
28+
expect(usage[0]?.data.stopReason).toBe("MAX_TOKENS");
29+
});
30+
31+
test("non-terminal event without finishReason emits no usage", () => {
32+
const adapter = createGoogleGenAIAdapter(TEST_SOURCE);
33+
const events = adapter.parseResponse(
34+
JSON.stringify({
35+
candidates: [
36+
{
37+
content: { parts: [{ text: "partial" }], role: "model" },
38+
index: 0,
39+
},
40+
],
41+
usageMetadata: { promptTokenCount: 5, candidatesTokenCount: 3 },
42+
}),
43+
);
44+
expect(events.some((e) => e.type === "inference.usage")).toBe(false);
45+
});
46+
});

vendor/intx-inference/src/providers/google-genai.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1461,7 +1461,14 @@ function parseResponse(
14611461
out.push({
14621462
type: "inference.usage",
14631463
seq,
1464-
data: { usage: tokenUsage, source },
1464+
// Locally patched — see vendor/intx-inference/PATCHES.md#inference-ts-cl-7783-truncated-tool-call
1465+
data: {
1466+
usage: tokenUsage,
1467+
...(candidate.finishReason === undefined
1468+
? {}
1469+
: { stopReason: candidate.finishReason }),
1470+
source,
1471+
},
14651472
});
14661473

14671474
// Terminal events seal the response. A still-pending

vendor/intx-types/PATCHES.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ seq 0. Companion to `store-ts-load-errors` in `@intx/storage-isogit` and
2222

2323
`src/runtime.ts` — The `inference.usage` variant of `InferenceEvent`
2424
gains optional `data.stopReason: string`, populated by adapters that
25-
observe a wire-level stop/finish reason (Anthropic `stop_reason`).
25+
observe a wire-level stop/finish reason (Anthropic `stop_reason`, Gemini
26+
`finishReason`).
2627
`inference.usage` is the only harness-level signal that records how a
2728
turn ended; without the provider's stop reason the harness cannot
2829
distinguish a complete turn from a truncation (`max_tokens` with a tool

0 commit comments

Comments
 (0)