From 2ca5fb40278a805fe7cb3dbd6fc7b5f26405448b Mon Sep 17 00:00:00 2001 From: ulofiai Date: Fri, 21 Aug 2026 07:17:36 +0800 Subject: [PATCH] fix: surface resolved model limits (#1608) Signed-off-by: ulofiai Co-authored-by: ulofiai --- base-action/src/run-claude-sdk.ts | 30 +++++++++ base-action/test/run-claude-sdk.test.ts | 89 +++++++++++++++++++++++++ docs/configuration.md | 23 +++++++ 3 files changed, 142 insertions(+) diff --git a/base-action/src/run-claude-sdk.ts b/base-action/src/run-claude-sdk.ts index 4d3ec73a..78d86652 100644 --- a/base-action/src/run-claude-sdk.ts +++ b/base-action/src/run-claude-sdk.ts @@ -82,6 +82,35 @@ async function createPromptConfig( return createMultiBlockMessage(); } +type ModelUsageSummary = Record< + string, + { + contextWindow: number; + maxOutputTokens: number; + } +>; + +/** + * Keep resolved model limits visible without exposing token usage or cost details. + */ +function sanitizeModelUsage( + modelUsage: SDKResultMessage["modelUsage"] | undefined, +): ModelUsageSummary | undefined { + if (!modelUsage) { + return undefined; + } + + return Object.fromEntries( + Object.entries(modelUsage).map(([model, usage]) => [ + model, + { + contextWindow: usage.contextWindow, + maxOutputTokens: usage.maxOutputTokens, + }, + ]), + ); +} + /** * Sanitizes SDK output to match CLI sanitization behavior */ @@ -119,6 +148,7 @@ function sanitizeSdkOutput( num_turns: resultMsg.num_turns, total_cost_usd: resultMsg.total_cost_usd, permission_denials_count: resultMsg.permission_denials?.length ?? 0, + modelUsage: sanitizeModelUsage(resultMsg.modelUsage), }, null, 2, diff --git a/base-action/test/run-claude-sdk.test.ts b/base-action/test/run-claude-sdk.test.ts index 0657fb9a..25adfa1f 100644 --- a/base-action/test/run-claude-sdk.test.ts +++ b/base-action/test/run-claude-sdk.test.ts @@ -64,6 +64,95 @@ describe("runClaudeWithSdk", () => { } }); + test("logs resolved model limits without exposing token usage", async () => { + const consoleLogSpy = spyOn(console, "log").mockImplementation(() => {}); + + tempDir = await mkdtemp(join(tmpdir(), "claude-sdk-")); + process.env.RUNNER_TEMP = tempDir; + + const promptPath = join(tempDir, "prompt.txt"); + await writeFile(promptPath, "test prompt"); + + const initMessage = { + type: "system", + subtype: "init", + session_id: "session-123", + model: "claude-opus-5", + }; + + const resultMessage = { + type: "result", + subtype: "success", + is_error: false, + duration_ms: 434, + num_turns: 1, + total_cost_usd: 1.23, + permission_denials: [], + modelUsage: { + "claude-opus-5": { + inputTokens: 96209, + outputTokens: 55324, + cacheReadInputTokens: 1135701, + cacheCreationInputTokens: 149043, + webSearchRequests: 0, + costUSD: 1.23, + contextWindow: 200000, + maxOutputTokens: 64000, + }, + }, + }; + + mock.module("@anthropic-ai/claude-agent-sdk", () => ({ + query: async function* () { + yield initMessage; + yield resultMessage; + }, + })); + + try { + const { runClaudeWithSdk } = await import("../src/run-claude-sdk"); + + await expect( + runClaudeWithSdk(promptPath, { + sdkOptions: {}, + showFullOutput: false, + hasJsonSchema: false, + }), + ).resolves.toMatchObject({ conclusion: "success" }); + + const sanitizedResult = consoleLogSpy.mock.calls + .map(([message]) => message) + .find( + (message) => + typeof message === "string" && message.includes('"type": "result"'), + ); + + expect(sanitizedResult).toBeDefined(); + if (typeof sanitizedResult !== "string") { + throw new Error("Sanitized result output was not logged"); + } + expect(JSON.parse(sanitizedResult)).toEqual({ + type: "result", + subtype: "success", + is_error: false, + duration_ms: 434, + num_turns: 1, + total_cost_usd: 1.23, + permission_denials_count: 0, + modelUsage: { + "claude-opus-5": { + contextWindow: 200000, + maxOutputTokens: 64000, + }, + }, + }); + expect(sanitizedResult).not.toContain("inputTokens"); + expect(sanitizedResult).not.toContain("costUSD"); + } finally { + consoleLogSpy.mockRestore(); + } + }); + test("fails when result subtype is success but is_error is true", async () => { const consoleErrorSpy = spyOn(console, "error").mockImplementation( () => {}, diff --git a/docs/configuration.md b/docs/configuration.md index 5c62d46d..989f2af7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -275,6 +275,29 @@ For provider-specific models: # ... other inputs ``` +### 1M context models through an API gateway + +When `ANTHROPIC_BASE_URL` points to an Anthropic-compatible API gateway, +Claude Code may not be able to verify that the gateway supports a model's native +1M context window and can budget the session at 200K instead. Append the +`[1m]` selector to explicitly use the 1M context window for supported models, +including Claude Opus 5 and Claude Sonnet 5: + +```yaml +- uses: anthropics/claude-code-action@v1 + with: + claude_args: | + --model "claude-opus-5[1m]" + # ... other inputs +``` + +Use the same selector when setting a model through `ANTHROPIC_MODEL` or another +Claude Code model environment variable. The selector is resolved by Claude Code +before requests are sent to the provider. The action's sanitized result output +includes each model's resolved +`contextWindow` and `maxOutputTokens` under `modelUsage`, so these limits are +visible without enabling `show_full_output`. + ## Claude Code Settings You can provide Claude Code settings to customize behavior such as model selection, environment variables, permissions, and hooks. Settings can be provided either as a JSON string or a path to a settings file.