@code-yeongyu/senpi-ai 2026.10.31-1 → 2026.11.2-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/api/anthropic-messages.d.ts +13 -3
  2. package/dist/api/anthropic-messages.js +60 -174
  3. package/dist/api/openai-completions.js +19 -2
  4. package/dist/api/openai-responses-shared.d.ts +2 -0
  5. package/dist/api/openai-responses-shared.js +19 -3
  6. package/dist/api/openai-responses.js +30 -9
  7. package/dist/auth/credential-store.d.ts +3 -1
  8. package/dist/auth/credential-store.js +78 -0
  9. package/dist/auth/helpers.d.ts +1 -0
  10. package/dist/auth/helpers.js +1 -0
  11. package/dist/auth/oauth/kimi-coding.js +6 -4
  12. package/dist/auth/oauth/openai-chatgpt-model-catalog.d.ts +36 -0
  13. package/dist/auth/oauth/openai-chatgpt-model-catalog.js +104 -0
  14. package/dist/auth/oauth/openai-chatgpt.js +76 -0
  15. package/dist/auth/oauth/radius.js +1 -2
  16. package/dist/auth/oauth-refresh.d.ts +2 -0
  17. package/dist/auth/oauth-refresh.js +53 -7
  18. package/dist/auth/pool/failover.d.ts +0 -40
  19. package/dist/auth/pool/failover.js +0 -76
  20. package/dist/auth/pool/slots.d.ts +11 -1
  21. package/dist/auth/pool/slots.js +73 -1
  22. package/dist/auth/resolve.js +3 -0
  23. package/dist/auth/types.d.ts +18 -0
  24. package/dist/compat/extension-oauth-types.d.ts +1 -0
  25. package/dist/models.d.ts +4 -5
  26. package/dist/models.js +37 -2
  27. package/dist/provider-diagnostic.d.ts +2 -0
  28. package/dist/provider-diagnostic.js +6 -1
  29. package/dist/providers/chatgpt-subscription.js +2 -0
  30. package/dist/providers/data/.manifest.json +1 -1
  31. package/dist/providers/data/opengateway.json +1 -1
  32. package/dist/providers/data/openrouter.json +1 -1
  33. package/dist/providers/data/vercel-ai-gateway.json +1 -1
  34. package/dist/types.d.ts +8 -0
  35. package/dist/utils/chatgpt-subscription-auth.d.ts +7 -0
  36. package/dist/utils/chatgpt-subscription-auth.js +21 -4
  37. package/dist/utils/empty-response-errors.d.ts +1 -2
  38. package/dist/utils/empty-response-errors.js +1 -2
  39. package/dist/utils/oauth-refresh-error.d.ts +8 -1
  40. package/dist/utils/oauth-refresh-error.js +3 -1
  41. package/dist/utils/prompt-cache-ttl.d.ts +2 -1
  42. package/dist/utils/prompt-cache-ttl.js +2 -0
  43. package/dist/utils/provider-diagnostic-carrier.d.ts +10 -1
  44. package/dist/utils/provider-diagnostic-carrier.js +35 -0
  45. package/dist/utils/provider-diagnostic-sources.d.ts +2 -0
  46. package/dist/utils/provider-diagnostic-sources.js +5 -0
  47. package/dist/utils/provider-diagnostic-vocabulary.d.ts +1 -0
  48. package/dist/utils/provider-diagnostic-vocabulary.js +9 -0
  49. package/dist/utils/provider-retry.js +4 -1
  50. package/dist/utils/retry.d.ts +2 -0
  51. package/dist/utils/retry.js +14 -3
  52. package/dist/utils/siwc-errors.d.ts +2 -0
  53. package/dist/utils/siwc-errors.js +2 -0
  54. package/dist/utils/tool-name-match.js +46 -1
  55. package/dist/utils/tool-schema-compat.js +35 -0
  56. package/dist/utils/validation.js +111 -4
  57. package/package.json +2 -2
  58. package/dist/auth/pool/classify.d.ts +0 -16
  59. package/dist/auth/pool/classify.js +0 -75
  60. package/dist/utils/claude-code-version-cache.d.ts +0 -12
  61. package/dist/utils/claude-code-version-cache.js +0 -60
  62. package/dist/utils/claude-code-version.d.ts +0 -78
  63. package/dist/utils/claude-code-version.js +0 -203
@@ -1,9 +1,7 @@
1
1
  import Anthropic from "@anthropic-ai/sdk";
2
2
  import type { MessageCreateParamsNonStreaming } from "@anthropic-ai/sdk/resources/beta/messages/messages.js";
3
- import type { AnthropicRefusalFallback, Context, Model, SimpleStreamOptions, StreamFunction, StreamOptions, Tool } from "../types.ts";
3
+ import type { AnthropicRefusalFallback, Context, Model, SimpleStreamOptions, StreamFunction, StreamOptions } from "../types.ts";
4
4
  export { getAnthropicCompat } from "../utils/prompt-cache-ttl.ts";
5
- export declare const toClaudeCodeName: (name: string) => string;
6
- export declare const fromClaudeCodeName: (name: string, tools?: Tool[]) => string;
7
5
  export type AnthropicEffort = "low" | "medium" | "high" | "xhigh" | "max";
8
6
  export type AnthropicThinkingDisplay = "summarized" | "omitted";
9
7
  export interface AnthropicOptions extends StreamOptions {
@@ -72,6 +70,18 @@ export interface AnthropicOptions extends StreamOptions {
72
70
  */
73
71
  client?: Anthropic;
74
72
  }
73
+ /**
74
+ * A Claude OAuth token (`sk-ant-oat...`) offered as an API key. senpi no longer sends such a token
75
+ * to the Messages API while presenting itself as Claude Code (senpi#5131); the subscription path is
76
+ * the `anthropic-subscription` provider, which runs the official Claude Code CLI.
77
+ */
78
+ export declare class ClaudeOAuthTokenRefusedError extends Error {
79
+ readonly code = "claude_oauth_token_refused";
80
+ readonly provider: string;
81
+ readonly modelId: string;
82
+ constructor(provider: string, modelId: string);
83
+ }
84
+ export declare function isClaudeOAuthTokenRefusedError(error: unknown): error is ClaudeOAuthTokenRefusedError;
75
85
  export declare const stream: StreamFunction<"anthropic-messages", AnthropicOptions>;
76
86
  export declare const streamSimple: StreamFunction<"anthropic-messages", SimpleStreamOptions>;
77
87
  export declare function buildAnthropicWarmPromptCacheParams(model: Model<"anthropic-messages">, context: Context, options?: AnthropicOptions): MessageCreateParamsNonStreaming;
@@ -6,7 +6,6 @@ import { registerSessionResourceCleanup } from "../session-resources.js";
6
6
  import { isVideoMimeType } from "../types.js";
7
7
  import { combineAbortSignals } from "../utils/abort-signals.js";
8
8
  import { pushAssistantProgress } from "../utils/assistant-capture.js";
9
- import { claudeCodeVersionTooOldHint, getClaudeCodeVersion, isClaudeCodeVersionTooOldError, recoverClaudeCodeVersion, } from "../utils/claude-code-version.js";
10
9
  import { splitDeferredTools } from "../utils/deferred-tools.js";
11
10
  import { appendAssistantMessageDiagnostic } from "../utils/diagnostics.js";
12
11
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
@@ -73,49 +72,6 @@ function getCacheControl(model, cacheRetention, env) {
73
72
  cacheControl: { type: "ephemeral", ...(ttl && { ttl }) },
74
73
  };
75
74
  }
76
- // Stealth mode: Mimic Claude Code's identity and tool naming exactly.
77
- // The bundled Claude Code version and the floor of the advertised `claude-cli/<version>`
78
- // (see utils/claude-code-version.ts). Keep this exact declaration: a downstream installer
79
- // (oh-my-openagent) rewrites it byte-for-byte in the installed dist and bundle.
80
- const claudeCodeVersion = "2.1.306";
81
- // Claude Code 2.x tool names (canonical casing)
82
- // Source: https://cchistory.mariozechner.at/data/prompts-2.1.11.md
83
- // To update: https://github.com/badlogic/cchistory
84
- const claudeCodeTools = [
85
- "Read",
86
- "Write",
87
- "Edit",
88
- "Bash",
89
- "Grep",
90
- "Glob",
91
- "AskUserQuestion",
92
- "EnterPlanMode",
93
- "ExitPlanMode",
94
- "KillShell",
95
- "NotebookEdit",
96
- "Skill",
97
- "Task",
98
- "TaskOutput",
99
- "TodoWrite",
100
- "WebFetch",
101
- "WebSearch",
102
- ];
103
- const ccToolLookup = new Map(claudeCodeTools.map((t) => [t.toLowerCase(), t]));
104
- const ccToolAliases = new Map([["ask_user_question", "AskUserQuestion"]]);
105
- // Convert tool name to CC canonical casing if it matches (case-insensitive)
106
- export const toClaudeCodeName = (name) => ccToolAliases.get(name) ?? ccToolLookup.get(name.toLowerCase()) ?? name;
107
- export const fromClaudeCodeName = (name, tools) => {
108
- if (tools && tools.length > 0) {
109
- const alias = [...ccToolAliases].find(([, wireName]) => wireName === name);
110
- if (alias && tools.some((tool) => tool.name === alias[0]))
111
- return alias[0];
112
- const lowerName = name.toLowerCase();
113
- const matchedTool = tools.find((tool) => tool.name.toLowerCase() === lowerName);
114
- if (matchedTool)
115
- return matchedTool.name;
116
- }
117
- return name;
118
- };
119
75
  function convertContentBlocks(content) {
120
76
  // If only text blocks, return as concatenated string for simplicity
121
77
  const hasImages = content.some((c) => c.type === "image");
@@ -323,6 +279,28 @@ function assertRequestAuth(provider, apiKey, headers) {
323
279
  if (!hasRequestAuth(apiKey, headers))
324
280
  throw new Error(`No API key for provider: ${provider}`);
325
281
  }
282
+ const CLAUDE_OAUTH_TOKEN_MARKER = "sk-ant-oat";
283
+ /**
284
+ * A Claude OAuth token (`sk-ant-oat...`) offered as an API key. senpi no longer sends such a token
285
+ * to the Messages API while presenting itself as Claude Code (senpi#5131); the subscription path is
286
+ * the `anthropic-subscription` provider, which runs the official Claude Code CLI.
287
+ */
288
+ export class ClaudeOAuthTokenRefusedError extends Error {
289
+ constructor(provider, modelId) {
290
+ super(`${provider}/${modelId}: a Claude OAuth token (${CLAUDE_OAUTH_TOKEN_MARKER}...) is not accepted as an API key; senpi does not send Claude subscription tokens to the Anthropic API directly. For a Claude Pro/Max subscription sign in with /login anthropic-subscription (or set CLAUDE_CODE_OAUTH_TOKEN) and use anthropic-subscription/${modelId}; otherwise use an Anthropic API key.`);
291
+ this.code = "claude_oauth_token_refused";
292
+ this.name = "ClaudeOAuthTokenRefusedError";
293
+ this.provider = provider;
294
+ this.modelId = modelId;
295
+ }
296
+ }
297
+ export function isClaudeOAuthTokenRefusedError(error) {
298
+ return error instanceof ClaudeOAuthTokenRefusedError;
299
+ }
300
+ function assertNotClaudeOAuthToken(model, apiKey) {
301
+ if (apiKey?.includes(CLAUDE_OAUTH_TOKEN_MARKER))
302
+ throw new ClaudeOAuthTokenRefusedError(model.provider, model.id);
303
+ }
326
304
  /**
327
305
  * Anthropic SDK client that never runs the SDK's own credential chain
328
306
  * (ANTHROPIC_PROFILE config files, federation env vars). Without this, every
@@ -410,7 +388,7 @@ function userMessageClosesServerTurn(message) {
410
388
  return message.content.trim().length > 0;
411
389
  return message.content.some((block) => (block.type === "text" && block.text.trim().length > 0) || block.type === "image");
412
390
  }
413
- function collectProviderNativeToolPairing(messages, model, deferredToolNames, normalizeToolName, discardedFallbackToolCallIds) {
391
+ function collectProviderNativeToolPairing(messages, model, deferredToolNames, discardedFallbackToolCallIds) {
414
392
  const resolvedUseIds = new Set();
415
393
  const validResultIds = new Set();
416
394
  // Uses whose turn can still be resumed. A same-model assistant resets the
@@ -462,10 +440,9 @@ function collectProviderNativeToolPairing(messages, model, deferredToolNames, no
462
440
  // and the pending use resumable.
463
441
  let emitsReferences = false;
464
442
  for (const name of message.addedToolNames ?? []) {
465
- const normalizedName = normalizeToolName(name);
466
- if (!deferredToolNames.has(normalizedName) || loadedReferenceNames.has(normalizedName))
443
+ if (!deferredToolNames.has(name) || loadedReferenceNames.has(name))
467
444
  continue;
468
- loadedReferenceNames.add(normalizedName);
445
+ loadedReferenceNames.add(name);
469
446
  emitsReferences = true;
470
447
  }
471
448
  if (emitsReferences)
@@ -926,7 +903,6 @@ async function* iterateAnthropicEvents(response, signal, onTransportActivity) {
926
903
  export const stream = (model, context, options) => {
927
904
  const stream = new AssistantMessageEventStream({ captureBlocks: true });
928
905
  const normalizedContext = resolveTranscript(context, supportsMidConvoSystemMessages(model));
929
- const currentTools = getCurrentTools(normalizedContext.messages);
930
906
  (async () => {
931
907
  const providerThinkingLevel = managedEffortForRequest(model, options);
932
908
  const output = {
@@ -953,8 +929,6 @@ export const stream = (model, context, options) => {
953
929
  const requestSignal = combinedAbort.signal;
954
930
  try {
955
931
  let client;
956
- let isOAuth = false;
957
- let advertisedClaudeCodeVersion;
958
932
  let usageModel = model;
959
933
  let inputTransformations;
960
934
  let openClient;
@@ -967,6 +941,7 @@ export const stream = (model, context, options) => {
967
941
  const federation = getAnthropicFederation(model, apiKey, optionsHeaders, options?.env);
968
942
  if (!federation)
969
943
  assertRequestAuth(model.provider, apiKey, optionsHeaders);
944
+ assertNotClaudeOAuthToken(model, apiKey);
970
945
  let copilotDynamicHeaders;
971
946
  if (model.provider === "github-copilot") {
972
947
  const hasImages = hasCopilotVisionInput(normalizedContext.messages);
@@ -978,37 +953,12 @@ export const stream = (model, context, options) => {
978
953
  const cacheRetention = resolveCacheRetention(options?.cacheRetention ?? model.cacheRetention, options?.env, "short");
979
954
  const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
980
955
  openClient = () => {
981
- const created = createClient(model, apiKey, options?.interleavedThinking ?? true, shouldUseFineGrainedToolStreamingBeta(model, normalizedContext), options?.refusalFallbacks !== undefined && configuredBetaHeader(model, options) !== null, optionsHeaders, getAnthropicCompat(model).supportsGzipRequestBody
956
+ client = createClient(model, apiKey, options?.interleavedThinking ?? true, shouldUseFineGrainedToolStreamingBeta(model, normalizedContext), options?.refusalFallbacks !== undefined && configuredBetaHeader(model, options) !== null, optionsHeaders, getAnthropicCompat(model).supportsGzipRequestBody
982
957
  ? withGzipRequestBodies(options?.fetch)
983
958
  : options?.fetch, copilotDynamicHeaders, cacheSessionId, options?.env, federation);
984
- client = created.client;
985
- isOAuth = created.isOAuthToken;
986
- advertisedClaudeCodeVersion = created.claudeCodeVersion;
987
959
  };
988
960
  openClient();
989
961
  }
990
- // One retry per request: a `claude_code_version_too_old` 400 names the version Anthropic
991
- // wants, so the fingerprint is raised and the same request goes out again with it.
992
- let claudeCodeVersionRetried = false;
993
- const retryWithNewerClaudeCode = async (error) => {
994
- if (claudeCodeVersionRetried ||
995
- openClient === undefined ||
996
- advertisedClaudeCodeVersion === undefined ||
997
- !isClaudeCodeVersionTooOldError(error)) {
998
- return false;
999
- }
1000
- const next = await recoverClaudeCodeVersion(error, claudeCodeVersion, advertisedClaudeCodeVersion, options?.env);
1001
- if (next === undefined)
1002
- return false;
1003
- claudeCodeVersionRetried = true;
1004
- openClient();
1005
- return true;
1006
- };
1007
- const noteTooOldClaudeCode = (error) => {
1008
- if (advertisedClaudeCodeVersion !== undefined && isClaudeCodeVersionTooOldError(error)) {
1009
- error.message = `${error.message}\n${claudeCodeVersionTooOldHint(advertisedClaudeCodeVersion)}`;
1010
- }
1011
- };
1012
962
  const fallbackKey = unsignedThinkingFallbackKey(model, options?.sessionId);
1013
963
  let unsignedThinkingReplay = fallbackKey && unsignedThinkingTextReplayFallbacks.has(fallbackKey)
1014
964
  ? "text"
@@ -1025,7 +975,7 @@ export const stream = (model, context, options) => {
1025
975
  }
1026
976
  let lastSentParams;
1027
977
  const createRequest = async () => {
1028
- let params = buildParams(model, normalizedContext, isOAuth, options, unsignedThinkingReplay);
978
+ let params = buildParams(model, normalizedContext, options, unsignedThinkingReplay);
1029
979
  const nextParams = await options?.onPayload?.(params, model);
1030
980
  if (nextParams !== undefined) {
1031
981
  params = nextParams;
@@ -1110,16 +1060,6 @@ export const stream = (model, context, options) => {
1110
1060
  }
1111
1061
  if (markNewlyExpiredSearches(error))
1112
1062
  return createRequestDemotingExpiredSearches();
1113
- if (await retryWithNewerClaudeCode(error)) {
1114
- try {
1115
- return await createRequest();
1116
- }
1117
- catch (retryError) {
1118
- noteTooOldClaudeCode(retryError);
1119
- throw retryError;
1120
- }
1121
- }
1122
- noteTooOldClaudeCode(error);
1123
1063
  throw error;
1124
1064
  }
1125
1065
  }, {
@@ -1232,9 +1172,7 @@ export const stream = (model, context, options) => {
1232
1172
  const block = {
1233
1173
  type: "toolCall",
1234
1174
  id: event.content_block.id,
1235
- name: isOAuth
1236
- ? fromClaudeCodeName(event.content_block.name, currentTools)
1237
- : event.content_block.name,
1175
+ name: event.content_block.name,
1238
1176
  arguments: isRecord(event.content_block.input)
1239
1177
  ? event.content_block.input
1240
1178
  : {},
@@ -1594,6 +1532,7 @@ export const streamSimple = (model, context, options) => {
1594
1532
  if (!getAnthropicFederation(model, apiKey, optionsHeaders, options?.env)) {
1595
1533
  assertRequestAuth(model.provider, apiKey, optionsHeaders);
1596
1534
  }
1535
+ assertNotClaudeOAuthToken(model, apiKey);
1597
1536
  const base = {
1598
1537
  ...buildBaseOptions(model, context, options, options?.apiKey),
1599
1538
  toolChoice: options?.toolChoice,
@@ -1625,9 +1564,6 @@ export const streamSimple = (model, context, options) => {
1625
1564
  thinkingBudgetTokens: Math.min(adjusted.thinkingBudget, Math.max(0, maxTokens - 1024)),
1626
1565
  });
1627
1566
  };
1628
- function isOAuthToken(apiKey) {
1629
- return apiKey.includes("sk-ant-oat");
1630
- }
1631
1567
  function createClient(model, apiKey, interleavedThinking, useFineGrainedToolStreamingBeta, useServerSideFallbackBeta, optionsHeaders, fetch, dynamicHeaders, sessionId, env, federation) {
1632
1568
  // Adaptive thinking models have interleaved thinking built in, so skip the beta header.
1633
1569
  const needsInterleavedBeta = interleavedThinking && !supportsAdaptiveThinking(model);
@@ -1657,7 +1593,7 @@ function createClient(model, apiKey, interleavedThinking, useFineGrainedToolStre
1657
1593
  ...(betaFeatures.length > 0 ? { "anthropic-beta": betaFeatures.join(",") } : {}),
1658
1594
  }, model.headers, optionsHeaders)),
1659
1595
  });
1660
- return { client, isOAuthToken: false };
1596
+ return client;
1661
1597
  }
1662
1598
  // Copilot: Bearer auth, selective betas.
1663
1599
  if (model.provider === "github-copilot") {
@@ -1673,26 +1609,7 @@ function createClient(model, apiKey, interleavedThinking, useFineGrainedToolStre
1673
1609
  ...(betaFeatures.length > 0 ? { "anthropic-beta": betaFeatures.join(",") } : {}),
1674
1610
  }, model.headers, dynamicHeaders, optionsHeaders)),
1675
1611
  });
1676
- return { client, isOAuthToken: false };
1677
- }
1678
- // OAuth: Bearer auth, Claude Code identity headers
1679
- if (apiKey && isOAuthToken(apiKey)) {
1680
- const advertisedClaudeCodeVersion = getClaudeCodeVersion(claudeCodeVersion, env);
1681
- const client = new PiAnthropic({
1682
- apiKey: null,
1683
- authToken: apiKey,
1684
- baseURL: model.baseUrl,
1685
- dangerouslyAllowBrowser: true,
1686
- fetch,
1687
- defaultHeaders: sanitizeAdaptiveThinkingHeaders(model, mergeClientHeaders(model, {
1688
- accept: "application/json",
1689
- "anthropic-dangerous-direct-browser-access": "true",
1690
- "anthropic-beta": ["claude-code-20250219", "oauth-2025-04-20", ...betaFeatures].join(","),
1691
- "user-agent": `claude-cli/${advertisedClaudeCodeVersion}`,
1692
- "x-app": "cli",
1693
- }, model.headers, optionsHeaders)),
1694
- });
1695
- return { client, isOAuthToken: true, claudeCodeVersion: advertisedClaudeCodeVersion };
1612
+ return client;
1696
1613
  }
1697
1614
  // API key, header-owned auth, or workload identity federation.
1698
1615
  const affinityCompat = getAnthropicCompat(model);
@@ -1721,9 +1638,9 @@ function createClient(model, apiKey, interleavedThinking, useFineGrainedToolStre
1721
1638
  });
1722
1639
  federationClient = { key, fetch, client };
1723
1640
  }
1724
- return { client: federationClient.client.withOptions({ defaultHeaders }), isOAuthToken: false };
1641
+ return federationClient.client.withOptions({ defaultHeaders });
1725
1642
  }
1726
- const client = new PiAnthropic({
1643
+ return new PiAnthropic({
1727
1644
  apiKey: apiKey ?? null,
1728
1645
  authToken: null,
1729
1646
  baseURL: model.baseUrl,
@@ -1731,13 +1648,12 @@ function createClient(model, apiKey, interleavedThinking, useFineGrainedToolStre
1731
1648
  fetch,
1732
1649
  defaultHeaders,
1733
1650
  });
1734
- return { client, isOAuthToken: false };
1735
1651
  }
1736
1652
  export function buildAnthropicWarmPromptCacheParams(model, context, options) {
1737
1653
  // Public fork entry (warmPromptCache, cache-keepalive): accepts a raw Context and builds
1738
1654
  // the same transcript the turn's stream() resolves, so the warmed prefix matches.
1739
1655
  const transcript = resolveTranscript(normalizeContext(context), supportsMidConvoSystemMessages(model));
1740
- const params = buildParams(model, transcript, false, {
1656
+ const params = buildParams(model, transcript, {
1741
1657
  ...options,
1742
1658
  maxTokens: 0,
1743
1659
  thinkingEnabled: undefined,
@@ -1759,14 +1675,12 @@ function configuredBetaHeader(model, options) {
1759
1675
  }
1760
1676
  return configured;
1761
1677
  }
1762
- function getBetaFeatures(model, context, isOAuthToken, shape, options) {
1678
+ function getBetaFeatures(model, context, shape, options) {
1763
1679
  const configured = configuredBetaHeader(model, options);
1764
1680
  if (configured === null)
1765
1681
  return [];
1766
1682
  // Betas the request body depends on: the API rejects the request without them.
1767
1683
  const required = [];
1768
- if (isOAuthToken)
1769
- required.push("claude-code-20250219", "oauth-2025-04-20");
1770
1684
  if (shape.midConvoEffort)
1771
1685
  required.push(MID_CONVERSATION_OUTPUT_CONFIG_BETA);
1772
1686
  if (shape.thinkingBinding)
@@ -1801,7 +1715,7 @@ function defaultOptionalBetas(model, context, options) {
1801
1715
  features.push(SERVER_SIDE_FALLBACK_BETA);
1802
1716
  return features;
1803
1717
  }
1804
- function buildParams(model, context, isOAuthToken, options, unsignedThinkingReplay = getAnthropicCompat(model).unsignedThinkingReplay) {
1718
+ function buildParams(model, context, options, unsignedThinkingReplay = getAnthropicCompat(model).unsignedThinkingReplay) {
1805
1719
  const compat = getAnthropicCompat(model);
1806
1720
  const { cacheControl } = getCacheControl(model, options?.cacheRetention ?? model.cacheRetention, options?.env);
1807
1721
  const initialSystemMessage = getInitialSystemMessage(context.messages);
@@ -1816,7 +1730,7 @@ function buildParams(model, context, isOAuthToken, options, unsignedThinkingRepl
1816
1730
  // old boundary (read) and the new one (written for the next step) are marked. Two checkpoints fit
1817
1731
  // only when the system block holds one, which then also covers the tools before it (senpi#3242).
1818
1732
  const stablePrefixMarkers = conversationMessages.filter((message) => message.role === "assistant" && message.promptCacheBreakpoint === true).length;
1819
- const systemHoldsCheckpoint = isOAuthToken || initialSystemText.length > 0;
1733
+ const systemHoldsCheckpoint = initialSystemText.length > 0;
1820
1734
  const stablePrefixCheckpointLimit = stablePrefixMarkers >= 2 && systemHoldsCheckpoint ? 2 : Math.min(1, stablePrefixMarkers);
1821
1735
  // Native tool changes reference tools by name, so a redefined name cannot be expressed,
1822
1736
  // and Anthropic rejects a tool list where every tool is deferred, so there must be an
@@ -1824,18 +1738,14 @@ function buildParams(model, context, isOAuthToken, options, unsignedThinkingRepl
1824
1738
  const initialTools = initialSystemMessage?.toolsAdded ?? [];
1825
1739
  // A configured `anthropic-beta: null` asks for no betas at all, so the request takes a shape that needs none:
1826
1740
  // the current tool list instead of native tool changes, top-level effort instead of per-message markers, and
1827
- // no server-side fallbacks (senpi#2957). OAuth still needs its identity betas; getBetaFeatures names them.
1741
+ // no server-side fallbacks (senpi#2957).
1828
1742
  const betasSuppressed = configuredBetaHeader(model, options) === null;
1829
- if (betasSuppressed && isOAuthToken) {
1830
- throw new Error(`The anthropic-beta header is set to null, but ${model.provider}/${model.id} is called with an OAuth token, which needs the claude-code-20250219 and oauth-2025-04-20 betas. Remove the null, or list the betas you want instead; senpi adds the ones a request needs.`);
1831
- }
1832
1743
  const midConvoEffort = model.compat?.supportsMidConvoEffort === true && !betasSuppressed;
1833
1744
  const nativeToolChanges = !betasSuppressed &&
1834
1745
  supportsMidConvoSystemMessages(model) &&
1835
1746
  supportsMidConvoToolChanges(model) &&
1836
1747
  initialTools.length > 0 &&
1837
1748
  !hasToolRedefinitions(context.messages);
1838
- const normalizeToolName = isOAuthToken ? toClaudeCodeName : (name) => name;
1839
1749
  // A marker on a discarded pre-fallback result must not defer its tool:
1840
1750
  // convertMessages drops that result, so no tool_reference would replay to
1841
1751
  // load the deferred definition.
@@ -1846,23 +1756,23 @@ function buildParams(model, context, isOAuthToken, options, unsignedThinkingRepl
1846
1756
  // `defer_loading` and loaded by a `tool_reference`. Native tool changes own deferral otherwise.
1847
1757
  const toolPlacement = nativeToolChanges
1848
1758
  ? { immediate: [], deferred: new Map() }
1849
- : splitDeferredTools({ tools: getCurrentTools(context.messages), messages: partitionMessages }, compat.supportsToolReferences, normalizeToolName);
1759
+ : splitDeferredTools({ tools: getCurrentTools(context.messages), messages: partitionMessages }, compat.supportsToolReferences);
1850
1760
  let immediateTools = toolPlacement.immediate;
1851
1761
  let deferredTools = [...toolPlacement.deferred.values()];
1852
1762
  if (immediateTools.length === 0 && deferredTools.length > 0) {
1853
1763
  immediateTools = deferredTools;
1854
1764
  deferredTools = [];
1855
1765
  }
1856
- const deferredToolNames = new Set(deferredTools.map((tool) => normalizeToolName(tool.name)));
1766
+ const deferredToolNames = new Set(deferredTools.map((tool) => tool.name));
1857
1767
  const activeEffort = managedEffortForRequest(model, options) ?? "high";
1858
1768
  const sendFallbacks = options?.refusalFallbacks !== undefined && !betasSuppressed;
1859
- const betaFeatures = getBetaFeatures(model, context, isOAuthToken, {
1769
+ const betaFeatures = getBetaFeatures(model, context, {
1860
1770
  midConvoEffort,
1861
1771
  thinkingBinding: midConvoEffort && options?.thinkingEnabled !== false,
1862
1772
  nativeToolChanges,
1863
1773
  refusalFallbacks: sendFallbacks,
1864
1774
  }, options);
1865
- const convertedMessages = convertMessages(conversationMessages, model, isOAuthToken, cacheControl, unsignedThinkingReplay, deferredToolNames, normalizeToolName, midConvoEffort ? model.provider : undefined, nativeToolChanges, stablePrefixCheckpointLimit);
1775
+ const convertedMessages = convertMessages(conversationMessages, model, cacheControl, unsignedThinkingReplay, deferredToolNames, midConvoEffort ? model.provider : undefined, nativeToolChanges, stablePrefixCheckpointLimit);
1866
1776
  const messages = midConvoEffort
1867
1777
  ? insertThinkingLevelMessages(convertedMessages, activeEffort)
1868
1778
  : convertedMessages.messages;
@@ -1888,21 +1798,7 @@ function buildParams(model, context, isOAuthToken, options, unsignedThinkingRepl
1888
1798
  stream: true,
1889
1799
  ...(betaFeatures.length > 0 ? { betas: betaFeatures } : {}),
1890
1800
  };
1891
- // For OAuth tokens, we MUST include Claude Code identity
1892
- if (isOAuthToken) {
1893
- params.system = [
1894
- {
1895
- type: "text",
1896
- text: "You are Claude Code, Anthropic's official CLI for Claude.",
1897
- ...(!initialSystemText && cacheControl ? { cache_control: cacheControl } : {}),
1898
- },
1899
- ];
1900
- if (initialSystemText) {
1901
- params.system.push(...anthropicSystemTextBlocks(initialSystemText, initialSystemMessage?.stablePrefix, cacheControl));
1902
- }
1903
- }
1904
- else if (initialSystemText) {
1905
- // Add cache control to system prompt for non-OAuth tokens
1801
+ if (initialSystemText) {
1906
1802
  params.system = anthropicSystemTextBlocks(initialSystemText, initialSystemMessage?.stablePrefix, cacheControl);
1907
1803
  }
1908
1804
  // Temperature is incompatible with extended thinking and unsupported on Claude Opus 4.7+.
@@ -1923,15 +1819,15 @@ function buildParams(model, context, isOAuthToken, options, unsignedThinkingRepl
1923
1819
  const initialNames = new Set(initialTools.map((tool) => tool.name));
1924
1820
  const laterTools = getDeclaredTools(context.messages).filter((tool) => !initialNames.has(tool.name));
1925
1821
  params.tools = [
1926
- ...convertTools(initialTools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl),
1822
+ ...convertTools(initialTools, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl),
1927
1823
  DEFERRED_TOOL_PLACEHOLDER,
1928
- ...convertTools(laterTools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, undefined, true),
1824
+ ...convertTools(laterTools, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, undefined, true),
1929
1825
  ];
1930
1826
  }
1931
1827
  else if (immediateTools.length > 0 || deferredTools.length > 0) {
1932
1828
  params.tools = [
1933
- ...convertTools(immediateTools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl),
1934
- ...convertTools(deferredTools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, undefined, true),
1829
+ ...convertTools(immediateTools, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl),
1830
+ ...convertTools(deferredTools, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, undefined, true),
1935
1831
  ];
1936
1832
  }
1937
1833
  // Managed effort models always use adaptive thinking so prefix mismatches can
@@ -2052,17 +1948,13 @@ function finalAssistantTurnStartsWithToolUse(messages) {
2052
1948
  }
2053
1949
  return false;
2054
1950
  }
2055
- function convertToolResult(msg, isOAuthToken, deferredToolNames, loadedToolNames, normalizeToolName) {
1951
+ function convertToolResult(msg, deferredToolNames, loadedToolNames) {
2056
1952
  const references = [];
2057
1953
  for (const name of msg.addedToolNames ?? []) {
2058
- const normalizedName = normalizeToolName(name);
2059
- if (!deferredToolNames.has(normalizedName) || loadedToolNames.has(normalizedName))
1954
+ if (!deferredToolNames.has(name) || loadedToolNames.has(name))
2060
1955
  continue;
2061
- loadedToolNames.add(normalizedName);
2062
- references.push({
2063
- type: "tool_reference",
2064
- tool_name: isOAuthToken ? toClaudeCodeName(name) : name,
2065
- });
1956
+ loadedToolNames.add(name);
1957
+ references.push({ type: "tool_reference", tool_name: name });
2066
1958
  }
2067
1959
  // The video block variant is not in the SDK's ContentBlockParam union, so the
2068
1960
  // converted array is cast through the same escape hatch as tool_reference blocks.
@@ -2082,7 +1974,7 @@ function convertToolResult(msg, isOAuthToken, deferredToolNames, loadedToolNames
2082
1974
  : convertedContent,
2083
1975
  };
2084
1976
  }
2085
- function convertMessages(transformedMessages, model, isOAuthToken, cacheControl, unsignedThinkingReplay = "text", deferredToolNames = new Set(), normalizeToolName = (name) => name, managedProvider, nativeToolChanges = false, stablePrefixCheckpointLimit = 0) {
1977
+ function convertMessages(transformedMessages, model, cacheControl, unsignedThinkingReplay = "text", deferredToolNames = new Set(), managedProvider, nativeToolChanges = false, stablePrefixCheckpointLimit = 0) {
2086
1978
  const params = [];
2087
1979
  const assistantLevels = new Map();
2088
1980
  const loadedToolNames = new Set();
@@ -2101,7 +1993,7 @@ function convertMessages(transformedMessages, model, isOAuthToken, cacheControl,
2101
1993
  // assistant turn below; drop their tool_results in lockstep so none dangle.
2102
1994
  const discardedFallbackToolCallIds = collectDiscardedFallbackToolCallIds(transformedMessages, model);
2103
1995
  const rejectsNativeWebSearchReplay = !getAnthropicCompat(model).supportsWebSearch;
2104
- const providerNativeToolPairing = collectProviderNativeToolPairing(transformedMessages, model, deferredToolNames, normalizeToolName, discardedFallbackToolCallIds);
1996
+ const providerNativeToolPairing = collectProviderNativeToolPairing(transformedMessages, model, deferredToolNames, discardedFallbackToolCallIds);
2105
1997
  for (let i = 0; i < transformedMessages.length; i++) {
2106
1998
  const msg = transformedMessages[i];
2107
1999
  if (msg.role === "system") {
@@ -2113,16 +2005,10 @@ function convertMessages(transformedMessages, model, isOAuthToken, cacheControl,
2113
2005
  blocks.push({ type: "text", text: sanitizeSurrogates(text) });
2114
2006
  if (nativeToolChanges) {
2115
2007
  for (const tool of msg.toolsRemoved ?? []) {
2116
- blocks.push({
2117
- type: "tool_removal",
2118
- tool: { type: "tool_reference", name: isOAuthToken ? toClaudeCodeName(tool.name) : tool.name },
2119
- });
2008
+ blocks.push({ type: "tool_removal", tool: { type: "tool_reference", name: tool.name } });
2120
2009
  }
2121
2010
  for (const tool of msg.toolsAdded ?? []) {
2122
- blocks.push({
2123
- type: "tool_addition",
2124
- tool: { type: "tool_reference", name: isOAuthToken ? toClaudeCodeName(tool.name) : tool.name },
2125
- });
2011
+ blocks.push({ type: "tool_addition", tool: { type: "tool_reference", name: tool.name } });
2126
2012
  }
2127
2013
  }
2128
2014
  if (blocks.length > 0)
@@ -2253,7 +2139,7 @@ function convertMessages(transformedMessages, model, isOAuthToken, cacheControl,
2253
2139
  blocks.push({
2254
2140
  type: "tool_use",
2255
2141
  id: block.id,
2256
- name: isOAuthToken ? toClaudeCodeName(block.name) : block.name,
2142
+ name: block.name,
2257
2143
  input: block.arguments ?? {},
2258
2144
  });
2259
2145
  }
@@ -2298,7 +2184,7 @@ function convertMessages(transformedMessages, model, isOAuthToken, cacheControl,
2298
2184
  while (j < transformedMessages.length && transformedMessages[j].role === "toolResult") {
2299
2185
  const nextMsg = transformedMessages[j]; // We know it's a toolResult
2300
2186
  if (!discardedFallbackToolCallIds.has(nextMsg.toolCallId)) {
2301
- const converted = convertToolResult(nextMsg, isOAuthToken, deferredToolNames, loadedToolNames, normalizeToolName);
2187
+ const converted = convertToolResult(nextMsg, deferredToolNames, loadedToolNames);
2302
2188
  toolResults.push(converted.toolResult);
2303
2189
  siblingContent.push(...converted.siblingContent);
2304
2190
  }
@@ -2390,7 +2276,7 @@ const isAnthropicStrictUnsupportedKeyword = (key, value) => {
2390
2276
  return typeof value !== "string" || !ANTHROPIC_STRICT_STRING_FORMATS.has(value);
2391
2277
  return false;
2392
2278
  };
2393
- function convertTools(tools, isOAuthToken, supportsEagerToolInputStreaming, supportsStrictTools, cacheControl, deferLoading = false) {
2279
+ function convertTools(tools, supportsEagerToolInputStreaming, supportsStrictTools, cacheControl, deferLoading = false) {
2394
2280
  if (!tools)
2395
2281
  return [];
2396
2282
  return tools.map((tool, index) => {
@@ -2411,7 +2297,7 @@ function convertTools(tools, isOAuthToken, supportsEagerToolInputStreaming, supp
2411
2297
  }
2412
2298
  : legacyInputSchema;
2413
2299
  return {
2414
- name: isOAuthToken ? toClaudeCodeName(tool.name) : tool.name,
2300
+ name: tool.name,
2415
2301
  description: tool.description,
2416
2302
  ...(supportsEagerToolInputStreaming ? { eager_input_streaming: true } : {}),
2417
2303
  ...(strict === true ? { strict: true } : {}),
@@ -1500,7 +1500,24 @@ export function convertMessages(model, context, compat, options = {}) {
1500
1500
  return params;
1501
1501
  }
1502
1502
  function convertTools(tools, compat) {
1503
- return tools.map((tool) => {
1503
+ const strictByTool = tools.map((tool) => tool.freeform || resolveGrammarConstrainedSampling(tool, compat.supportsOpenAIGrammarTools)
1504
+ ? undefined
1505
+ : resolveJsonSchemaStrictSampling(tool, compat.supportsStrictMode !== false));
1506
+ if (compat.mixedStrictTools === false) {
1507
+ const nonStrictTool = tools.find((tool, index) => !tool.freeform &&
1508
+ !resolveGrammarConstrainedSampling(tool, compat.supportsOpenAIGrammarTools) &&
1509
+ strictByTool[index] !== true);
1510
+ if (nonStrictTool) {
1511
+ const requiredTool = tools.find((tool) => tool.constrainedSampling &&
1512
+ tool.constrainedSampling.type === "json_schema" &&
1513
+ tool.constrainedSampling.strict === "require");
1514
+ if (requiredTool) {
1515
+ throw new Error(`Tool "${requiredTool.name}" requires JSON-schema constrained sampling, but tool "${nonStrictTool.name}" cannot be strict and this provider rejects mixed strict and non-strict tools.`);
1516
+ }
1517
+ strictByTool.fill(undefined);
1518
+ }
1519
+ }
1520
+ return tools.map((tool, index) => {
1504
1521
  const grammar = resolveGrammarConstrainedSampling(tool, compat.supportsOpenAIGrammarTools);
1505
1522
  if (grammar) {
1506
1523
  return {
@@ -1521,7 +1538,7 @@ function convertTools(tools, compat) {
1521
1538
  if (tool.freeform) {
1522
1539
  throw new Error("Freeform tools cannot be sent to OpenAI Chat Completions; use Responses API");
1523
1540
  }
1524
- const strict = resolveJsonSchemaStrictSampling(tool, compat.supportsStrictMode !== false);
1541
+ const strict = strictByTool[index];
1525
1542
  const schemaParameters = getJsonSchemaToolParameters(tool, strict);
1526
1543
  const normalizedParameters = compat.toolSchemaFlavor === "moonshot-mfjs"
1527
1544
  ? normalizeToolParametersForMoonshot(schemaParameters)
@@ -2,6 +2,8 @@ import type { Tool as OpenAITool, ResponseCreateParamsStreaming, ResponseInput,
2
2
  import type { Api, AssistantMessage, Message, Model, StreamOptions, Tool, TranscriptContext, Usage } from "../types.ts";
3
3
  import type { AssistantMessageEventStream } from "../utils/event-stream.ts";
4
4
  export interface OpenAIResponsesStreamOptions {
5
+ /** SIWC succeeds only on response.completed, never on a finished tool alone. */
6
+ siwc?: boolean;
5
7
  onProviderStreamEvent?: StreamOptions["onProviderStreamEvent"];
6
8
  serviceTier?: ResponseCreateParamsStreaming["service_tier"] | "fast" | "ultrafast";
7
9
  grammarToolInputProperties?: ReadonlyMap<string, string>;
@@ -1029,12 +1029,22 @@ export async function processResponsesStream(openaiStream, output, stream, model
1029
1029
  }
1030
1030
  else if (event.type === "response.completed" || event.type === "response.incomplete") {
1031
1031
  finalizeResponse(event.response);
1032
+ if (options?.siwc && event.type === "response.incomplete") {
1033
+ throw new Error(`Response incomplete: ${event.response.incomplete_details?.reason ?? "unknown reason"}`);
1034
+ }
1032
1035
  }
1033
1036
  else if (event.type === "error") {
1034
1037
  const errorEvent = event;
1035
1038
  const code = errorEvent.error?.code ?? errorEvent.code ?? errorEvent.status;
1036
1039
  const message = errorEvent.error?.message || errorEvent.message || "Unknown error";
1037
- throw new Error(code == null ? message : `Error Code ${code}: ${message}`);
1040
+ const failure = new Error(code == null ? message : `Error Code ${code}: ${message}`);
1041
+ throw options?.siwc
1042
+ ? Object.assign(failure, {
1043
+ code: typeof code === "string" ? code : undefined,
1044
+ status: errorEvent.status,
1045
+ param: errorEvent.error?.param ?? errorEvent.param,
1046
+ })
1047
+ : failure;
1038
1048
  }
1039
1049
  else if (event.type === "response.failed") {
1040
1050
  sawTerminalResponseEvent = true;
@@ -1046,11 +1056,17 @@ export async function processResponsesStream(openaiStream, output, stream, model
1046
1056
  : details?.reason
1047
1057
  ? `incomplete: ${details.reason}`
1048
1058
  : "Unknown error (no error details in response)";
1049
- throw new Error(msg);
1059
+ const failure = new Error(msg);
1060
+ throw options?.siwc
1061
+ ? Object.assign(failure, {
1062
+ code: error?.code,
1063
+ param: error && "param" in error ? error.param : undefined,
1064
+ })
1065
+ : failure;
1050
1066
  }
1051
1067
  }
1052
1068
  const hasFinalizedToolCall = output.content.some((block) => block.type === "toolCall" && !("partialJson" in block));
1053
- if (!sawTerminalResponseEvent && !hasFinalizedToolCall) {
1069
+ if (!sawTerminalResponseEvent && (options?.siwc || !hasFinalizedToolCall)) {
1054
1070
  throw new Error("OpenAI Responses stream ended before a terminal response event");
1055
1071
  }
1056
1072
  // The agent runs every tool call in the final message. Refuse to hand over calls whose