@gajae-code/ai 0.4.3 → 0.4.5

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.4.5] - 2026-06-12
6
+
7
+ ### Changed
8
+
9
+ - Bumped the spoofed Gemini CLI User-Agent version to 0.46.0 to track the upstream release.
10
+
11
+ ### Fixed
12
+
13
+ - Fixed direct Anthropic requests for Claude Fable/Mythos-style models that support tools but reject forced tool use by omitting forced `tool_choice` while preserving `auto`/`none` choices.
14
+ - Preserved catalog transport metadata for opencode-go `qwen3.7-max` model resolution.
15
+ - Set SQLite auth-store `busy_timeout` before enabling WAL so initialization is reliable under contention.
16
+ - Resolved provider credentials from inherited or GJC-owned environment sources instead of trusting the caller project's `.env` overlays.
17
+ - Rendered and executed Cursor-native tool calls without dropping provider-specific call details.
18
+
19
+ ## [0.4.4] - 2026-06-10
20
+
21
+ - Version aligned with the 0.4.4 monorepo release; no functional changes in this package.
22
+
5
23
  ## [0.4.2] - 2026-06-09
6
24
 
7
25
  ### Fixed
@@ -5,8 +5,8 @@ import { AssistantMessageEventStream } from "./utils/event-stream";
5
5
  /**
6
6
  * Get API key for provider from known environment variables, e.g. OPENAI_API_KEY.
7
7
  *
8
- * Will not return API keys for providers that require OAuth tokens.
9
- * Checks Bun.env, then cwd/.env, then ~/.env.
8
+ * Provider authentication intentionally excludes cwd/.env values. Project dotenv files are
9
+ * loaded into $env for app/tool execution, but must not silently fund GJC model requests.
10
10
  */
11
11
  export declare function getEnvApiKey(provider: string): string | undefined;
12
12
  /**
@@ -674,6 +674,10 @@ export interface AnthropicCompat {
674
674
  disableAdaptiveThinking?: boolean;
675
675
  /** Whether tools may include Anthropic's per-tool eager_input_streaming flag. Default: true. */
676
676
  supportsEagerToolInputStreaming?: boolean;
677
+ /** Whether the provider accepts the `tool_choice` parameter at all. Default: true. */
678
+ supportsToolChoice?: boolean;
679
+ /** Whether `tool_choice` may force a tool (`any` / named `tool`). Default: true except known incompatible Anthropic models. */
680
+ supportsForcedToolChoice?: boolean;
677
681
  /** Whether long prompt-cache retention (`ttl: "1h"`) is supported. Default: true for canonical Anthropic API. */
678
682
  supportsLongCacheRetention?: boolean;
679
683
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/ai",
4
- "version": "0.4.3",
4
+ "version": "0.4.5",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://gaebal-gajae.dev",
7
7
  "author": "Yeachan-Heo",
@@ -43,7 +43,7 @@
43
43
  "dependencies": {
44
44
  "@anthropic-ai/sdk": "^0.94.0",
45
45
  "@bufbuild/protobuf": "^2.12.0",
46
- "@gajae-code/utils": "0.4.3",
46
+ "@gajae-code/utils": "0.4.5",
47
47
  "openai": "^6.36.0",
48
48
  "partial-json": "^0.1.7",
49
49
  "zod": "4.4.3"
@@ -3621,10 +3621,14 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
3621
3621
  }
3622
3622
 
3623
3623
  #initializeSchema(): void {
3624
+ // Apply busy_timeout FIRST: `PRAGMA journal_mode=WAL` needs a brief
3625
+ // exclusive lock, and without an active busy_timeout a concurrent writer
3626
+ // makes it fail immediately with SQLITE_BUSY (deterministic on Windows,
3627
+ // where file locks are mandatory).
3628
+ this.#db.run("PRAGMA busy_timeout=5000");
3624
3629
  this.#db.run(`
3625
3630
  PRAGMA journal_mode=WAL;
3626
3631
  PRAGMA synchronous=NORMAL;
3627
- PRAGMA busy_timeout=5000;
3628
3632
  CREATE TABLE IF NOT EXISTS auth_schema_version (
3629
3633
  id INTEGER PRIMARY KEY CHECK (id = 1),
3630
3634
  version INTEGER NOT NULL
@@ -134,7 +134,13 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
134
134
  cache.staticFingerprint === staticFingerprint &&
135
135
  cache.staticFingerprint.length > 0
136
136
  ) {
137
- return { models: passModelList<TApi>(cache.models), stale: false };
137
+ const cachedModels = passModelList<TApi>(cache.models);
138
+ if (!hasStaticTransportDrift(staticModels, cachedModels)) {
139
+ return { models: cachedModels, stale: false };
140
+ }
141
+ const repairedModels = mergeDynamicModels(staticModels, cachedModels);
142
+ writeModelCache(options.providerId, now(), repairedModels, true, staticFingerprint, dbPath);
143
+ return { models: repairedModels, stale: false };
138
144
  }
139
145
 
140
146
  const [fetchedModelsDevModels, fetchedDynamicModels] = shouldFetchFromNetwork
@@ -229,6 +235,20 @@ function shouldFetchRemoteSources(
229
235
  return false;
230
236
  }
231
237
 
238
+ function hasStaticTransportDrift<TApi extends Api>(
239
+ staticModels: readonly Model<TApi>[],
240
+ cachedModels: readonly Model<TApi>[],
241
+ ): boolean {
242
+ if (staticModels.length === 0 || cachedModels.length === 0) return false;
243
+ const cachedById = new Map(cachedModels.map(model => [model.id, model]));
244
+ for (const staticModel of staticModels) {
245
+ const cachedModel = cachedById.get(staticModel.id);
246
+ if (!cachedModel) continue;
247
+ if (cachedModel.api !== staticModel.api) return true;
248
+ }
249
+ return false;
250
+ }
251
+
232
252
  function mergeModelSources<TApi extends Api>(...sources: readonly (readonly Model<TApi>[])[]): Model<TApi>[] {
233
253
  // Strip out empty/missing sources up front. The hot path is `(static, [])`
234
254
  // (modelsDev disabled / failed) — a single non-empty source means we can
@@ -292,9 +312,21 @@ function fingerprintStatic<TApi extends Api>(models: readonly Model<TApi>[]): st
292
312
 
293
313
  function mergeDynamicModel<TApi extends Api>(existingModel: Model<TApi>, dynamicModel: Model<TApi>): Model<TApi> {
294
314
  const supportsImage = existingModel.input.includes("image") || dynamicModel.input.includes("image");
315
+ // The static catalog is authoritative for transport: `api` (and its
316
+ // api-specific `baseUrl`). Dynamic discovery enumerates ids via a single
317
+ // hardcoded api (e.g. fetchOpenAICompatibleModels always tags
318
+ // `openai-completions`), so spreading it blindly would clobber catalog
319
+ // entries that route through a different format — e.g. opencode-go
320
+ // qwen3.7-max is `anthropic-messages` but would be downgraded to
321
+ // `openai-completions` and 401 with `not supported for format oa-compat`
322
+ // (issue #489). Keep the existing api, and only take the dynamic baseUrl
323
+ // when the api matches (same transport, same URL shape).
324
+ const baseUrl = existingModel.api === dynamicModel.api ? dynamicModel.baseUrl : existingModel.baseUrl;
295
325
  return enrichModelThinking({
296
326
  ...existingModel,
297
327
  ...dynamicModel,
328
+ api: existingModel.api,
329
+ baseUrl,
298
330
  name: preferDiscoveryName(dynamicModel.name, existingModel.name, dynamicModel.id),
299
331
  reasoning: existingModel.reasoning || dynamicModel.reasoning,
300
332
  input: supportsImage ? ["text", "image"] : ["text"],
@@ -881,6 +881,8 @@ function getAnthropicCompat(
881
881
  disableAdaptiveThinking: model.compat?.disableAdaptiveThinking ?? false,
882
882
  supportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? true,
883
883
  supportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? true,
884
+ supportsToolChoice: model.compat?.supportsToolChoice ?? true,
885
+ supportsForcedToolChoice: model.compat?.supportsForcedToolChoice ?? true,
884
886
  };
885
887
  }
886
888
 
@@ -1682,6 +1684,30 @@ function disableThinkingIfToolChoiceForced(params: MessageCreateParamsStreaming)
1682
1684
  }
1683
1685
  }
1684
1686
 
1687
+ function isForcedAnthropicToolChoice(toolChoice: NonNullable<AnthropicOptions["toolChoice"]>): boolean {
1688
+ if (typeof toolChoice === "string") return toolChoice === "any";
1689
+ if ("function" in toolChoice) return true;
1690
+ if ("name" in toolChoice && typeof toolChoice.name === "string") return true;
1691
+ return toolChoice.type === "tool" || toolChoice.type === "function";
1692
+ }
1693
+
1694
+ function supportsForcedAnthropicToolChoice(model: Model<"anthropic-messages">): boolean {
1695
+ const compat = model.compat;
1696
+ if (compat?.supportsToolChoice === false || compat?.supportsForcedToolChoice === false) return false;
1697
+
1698
+ // Claude Mythos Preview is documented as accepting tools while rejecting
1699
+ // forced tool use; Claude Fable currently returns the same Anthropic 400.
1700
+ return !/^claude-(?:fable|mythos)(?:-|$)/i.test(model.id);
1701
+ }
1702
+
1703
+ function shouldSendAnthropicToolChoice(
1704
+ model: Model<"anthropic-messages">,
1705
+ toolChoice: NonNullable<AnthropicOptions["toolChoice"]>,
1706
+ ): boolean {
1707
+ if (model.compat?.supportsToolChoice === false) return false;
1708
+ return !isForcedAnthropicToolChoice(toolChoice) || supportsForcedAnthropicToolChoice(model);
1709
+ }
1710
+
1685
1711
  function ensureMaxTokensForThinking(params: MessageCreateParamsStreaming, model: Model<"anthropic-messages">): void {
1686
1712
  const thinking = params.thinking;
1687
1713
  if (thinking?.type !== "enabled") return;
@@ -2029,7 +2055,7 @@ function buildParams(
2029
2055
  (params as ParamsWithSpeed).speed = "fast";
2030
2056
  }
2031
2057
 
2032
- if (options?.toolChoice) {
2058
+ if (options?.toolChoice && shouldSendAnthropicToolChoice(model, options.toolChoice)) {
2033
2059
  if (typeof options.toolChoice === "string") {
2034
2060
  params.tool_choice = { type: options.toolChoice };
2035
2061
  } else if (isOAuthToken && options.toolChoice.name) {
@@ -569,7 +569,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
569
569
  return stream;
570
570
  };
571
571
 
572
- type ToolCallState = ToolCall & { index: number; partialJson?: string; kind: "mcp" | "todo_write" };
572
+ type ToolCallState = ToolCall & { index: number; partialJson?: string; kind: "mcp" | "todo_write" | "native" };
573
573
 
574
574
  interface BlockState {
575
575
  currentTextBlock: (TextContent & { index: number }) | null;
@@ -1844,6 +1844,60 @@ function buildTodoWriteArgs(toolCall: CursorUpdateTodosToolCall): {
1844
1844
  };
1845
1845
  }
1846
1846
 
1847
+ // Map a cursor ToolCall oneof field name (e.g. "shellToolCall") to a display tool
1848
+ // name. Mirrors cli-jaw's cursorToolKindLabel (src/agent/events/cursor.ts) so the
1849
+ // two surfaces label cursor-native tools identically.
1850
+ const CURSOR_NATIVE_KIND_ALIASES: Record<string, string> = {
1851
+ shell: "bash",
1852
+ read: "read",
1853
+ write: "write",
1854
+ delete: "delete",
1855
+ edit: "edit",
1856
+ grep: "grep",
1857
+ glob: "glob",
1858
+ ls: "ls",
1859
+ semSearch: "codebase_search",
1860
+ webSearch: "web_search",
1861
+ fetch: "fetch",
1862
+ task: "task",
1863
+ createPlan: "create_plan",
1864
+ askQuestion: "ask_question",
1865
+ readLints: "read_lints",
1866
+ applyAgentDiff: "apply_diff",
1867
+ };
1868
+
1869
+ function cursorNativeToolName(kindKey: string): string {
1870
+ const base = kindKey.replace(/ToolCall$/i, "");
1871
+ if (!base) return "tool";
1872
+ return CURSOR_NATIVE_KIND_ALIASES[base] ?? base;
1873
+ }
1874
+
1875
+ // Cursor's model sometimes calls its own native IDE tools (shell/glob/grep/…)
1876
+ // instead of the advertised MCP tools. Those arrive as ToolCall oneof variants we
1877
+ // do not otherwise handle (everything except mcpToolCall / updateTodosToolCall), so
1878
+ // without this they are silently dropped and never render. Build a generic toolCall
1879
+ // block from whichever *ToolCall field is set so the call (and its result) is shown.
1880
+ function buildNativeToolCallBlock(
1881
+ toolCall: Record<string, unknown>,
1882
+ callId: string,
1883
+ index: number,
1884
+ ): ToolCallState | null {
1885
+ for (const [key, payload] of Object.entries(toolCall)) {
1886
+ if (!/ToolCall$/.test(key) || !payload || typeof payload !== "object") continue;
1887
+ if (key === "mcpToolCall" || key === "updateTodosToolCall") continue;
1888
+ const args = (payload as { args?: unknown }).args;
1889
+ return {
1890
+ type: "toolCall",
1891
+ id: callId,
1892
+ name: cursorNativeToolName(key),
1893
+ arguments: args && typeof args === "object" ? (args as Record<string, unknown>) : { raw: payload },
1894
+ index,
1895
+ kind: "native",
1896
+ };
1897
+ }
1898
+ return null;
1899
+ }
1900
+
1847
1901
  function buildMcpResultFromToolResult(_mcpCall: CursorMcpCall, toolResult: ToolResultMessage) {
1848
1902
  if (toolResult.isError) {
1849
1903
  return buildMcpErrorResult(toolResultToText(toolResult) || "MCP tool failed");
@@ -1987,6 +2041,21 @@ function processInteractionUpdate(
1987
2041
  output.content.push(block);
1988
2042
  state.setToolCall(block);
1989
2043
  stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
2044
+ return;
2045
+ }
2046
+
2047
+ // Fallback: cursor-native tool variants (shell/glob/grep/…) we don't model
2048
+ // explicitly. Render them so the call and its result are visible instead of
2049
+ // vanishing.
2050
+ const nativeBlock = buildNativeToolCallBlock(
2051
+ toolCall,
2052
+ update.message.value.callId || crypto.randomUUID(),
2053
+ output.content.length,
2054
+ );
2055
+ if (nativeBlock) {
2056
+ output.content.push(nativeBlock);
2057
+ state.setToolCall(nativeBlock);
2058
+ stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
1990
2059
  }
1991
2060
  }
1992
2061
  } else if (updateCase === "toolCallDelta" || updateCase === "partialToolCall") {
@@ -4,7 +4,7 @@
4
4
  * GeminiCLI/VERSION/MODEL (PLATFORM; ARCH; SURFACE)
5
5
  */
6
6
  export function getGeminiCliUserAgent(modelId = "gemini-3.1-pro-preview"): string {
7
- const version = process.env.PI_AI_GEMINI_CLI_VERSION || "0.45.2";
7
+ const version = process.env.PI_AI_GEMINI_CLI_VERSION || "0.46.0";
8
8
  const platform = process.platform === "win32" ? "win32" : process.platform;
9
9
  const arch = process.arch === "x64" ? "x64" : process.arch;
10
10
  return `GeminiCLI/${version}/${modelId} (${platform}; ${arch}; terminal)`;
package/src/stream.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import { $env, $inheritedEnv, $pickenv, extractHttpStatusFromError } from "@gajae-code/utils";
4
+ import { $credentialEnv, $env, $pickCredentialEnv, extractHttpStatusFromError } from "@gajae-code/utils";
5
5
  import { getCustomApi } from "./api-registry";
6
6
  import type { Effort } from "./model-thinking";
7
7
  import {
@@ -61,7 +61,7 @@ let cachedVertexAdcCredentialsExists: boolean | null = null;
61
61
 
62
62
  function hasVertexAdcCredentials(): boolean {
63
63
  if (cachedVertexAdcCredentialsExists === null) {
64
- const gacPath = $env.GOOGLE_APPLICATION_CREDENTIALS;
64
+ const gacPath = $credentialEnv("GOOGLE_APPLICATION_CREDENTIALS");
65
65
  if (gacPath) {
66
66
  cachedVertexAdcCredentialsExists = fs.existsSync(gacPath);
67
67
  } else {
@@ -77,7 +77,7 @@ type KeyResolver = string | (() => string | undefined);
77
77
 
78
78
  const serviceProviderMap: Record<string, KeyResolver> = {
79
79
  "alibaba-coding-plan": "ALIBABA_CODING_PLAN_API_KEY",
80
- openai: () => $inheritedEnv("OPENAI_API_KEY") ?? $env.OPENAI_API_KEY,
80
+ openai: () => $credentialEnv("OPENAI_API_KEY"),
81
81
  google: "GEMINI_API_KEY",
82
82
  groq: "GROQ_API_KEY",
83
83
  cerebras: "CEREBRAS_API_KEY",
@@ -107,18 +107,18 @@ const serviceProviderMap: Record<string, KeyResolver> = {
107
107
  parallel: "PARALLEL_API_KEY",
108
108
  kagi: "KAGI_API_KEY",
109
109
  // GitHub Copilot uses GitHub personal access token
110
- "github-copilot": () => $pickenv("COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"),
110
+ "github-copilot": () => $pickCredentialEnv("COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"),
111
111
  // Foundry mode optionally switches Anthropic auth to enterprise gateway credentials.
112
112
  anthropic: () =>
113
113
  isFoundryEnabled()
114
- ? $pickenv("ANTHROPIC_FOUNDRY_API_KEY", "ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY")
115
- : $pickenv("ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"),
114
+ ? $pickCredentialEnv("ANTHROPIC_FOUNDRY_API_KEY", "ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY")
115
+ : $pickCredentialEnv("ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"),
116
116
  "gitlab-duo": "GITLAB_TOKEN",
117
117
  // Vertex AI supports either GOOGLE_CLOUD_API_KEY or Application Default Credentials.
118
118
  "google-vertex": () => {
119
- if ($env.GOOGLE_CLOUD_API_KEY) {
120
- return $env.GOOGLE_CLOUD_API_KEY;
121
- }
119
+ const googleCloudApiKey = $credentialEnv("GOOGLE_CLOUD_API_KEY");
120
+ if (googleCloudApiKey) return googleCloudApiKey;
121
+
122
122
  const hasCredentials = hasVertexAdcCredentials();
123
123
  const hasProject = !!($env.GOOGLE_CLOUD_PROJECT || $env.GCLOUD_PROJECT);
124
124
  const hasLocation = !!$env.GOOGLE_CLOUD_LOCATION;
@@ -133,13 +133,18 @@ const serviceProviderMap: Record<string, KeyResolver> = {
133
133
  // 4. AWS_CONTAINER_CREDENTIALS_* - ECS/Task IAM role credentials
134
134
  // 5. AWS_WEB_IDENTITY_TOKEN_FILE + AWS_ROLE_ARN - IRSA (EKS) web identity
135
135
  "amazon-bedrock": () => {
136
+ const awsProfile = $credentialEnv("AWS_PROFILE");
137
+ const awsAccessKeyId = $credentialEnv("AWS_ACCESS_KEY_ID");
138
+ const awsSecretAccessKey = $credentialEnv("AWS_SECRET_ACCESS_KEY");
139
+ const awsBearerToken = $credentialEnv("AWS_BEARER_TOKEN_BEDROCK");
136
140
  const hasEcsCredentials =
137
- !!$env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || !!$env.AWS_CONTAINER_CREDENTIALS_FULL_URI;
138
- const hasWebIdentity = !!$env.AWS_WEB_IDENTITY_TOKEN_FILE && !!$env.AWS_ROLE_ARN;
141
+ !!$credentialEnv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") ||
142
+ !!$credentialEnv("AWS_CONTAINER_CREDENTIALS_FULL_URI");
143
+ const hasWebIdentity = !!$credentialEnv("AWS_WEB_IDENTITY_TOKEN_FILE") && !!$credentialEnv("AWS_ROLE_ARN");
139
144
  if (
140
- $env.AWS_PROFILE ||
141
- ($env.AWS_ACCESS_KEY_ID && $env.AWS_SECRET_ACCESS_KEY) ||
142
- $env.AWS_BEARER_TOKEN_BEDROCK ||
145
+ awsProfile ||
146
+ (awsAccessKeyId && awsSecretAccessKey) ||
147
+ awsBearerToken ||
143
148
  hasEcsCredentials ||
144
149
  hasWebIdentity
145
150
  ) {
@@ -148,7 +153,7 @@ const serviceProviderMap: Record<string, KeyResolver> = {
148
153
  },
149
154
  synthetic: "SYNTHETIC_API_KEY",
150
155
  "cloudflare-ai-gateway": "CLOUDFLARE_AI_GATEWAY_API_KEY",
151
- huggingface: () => $pickenv("HUGGINGFACE_HUB_TOKEN", "HF_TOKEN"),
156
+ huggingface: () => $pickCredentialEnv("HUGGINGFACE_HUB_TOKEN", "HF_TOKEN"),
152
157
  litellm: "LITELLM_API_KEY",
153
158
  moonshot: "MOONSHOT_API_KEY",
154
159
  nvidia: "NVIDIA_API_KEY",
@@ -158,7 +163,7 @@ const serviceProviderMap: Record<string, KeyResolver> = {
158
163
  "ollama-cloud": "OLLAMA_CLOUD_API_KEY",
159
164
  "llama.cpp": "LLAMA_CPP_API_KEY",
160
165
  qianfan: "QIANFAN_API_KEY",
161
- "qwen-portal": () => $pickenv("QWEN_OAUTH_TOKEN", "QWEN_PORTAL_API_KEY"),
166
+ "qwen-portal": () => $pickCredentialEnv("QWEN_OAUTH_TOKEN", "QWEN_PORTAL_API_KEY"),
162
167
  together: "TOGETHER_API_KEY",
163
168
  zenmux: "ZENMUX_API_KEY",
164
169
  venice: "VENICE_API_KEY",
@@ -169,13 +174,13 @@ const serviceProviderMap: Record<string, KeyResolver> = {
169
174
  /**
170
175
  * Get API key for provider from known environment variables, e.g. OPENAI_API_KEY.
171
176
  *
172
- * Will not return API keys for providers that require OAuth tokens.
173
- * Checks Bun.env, then cwd/.env, then ~/.env.
177
+ * Provider authentication intentionally excludes cwd/.env values. Project dotenv files are
178
+ * loaded into $env for app/tool execution, but must not silently fund GJC model requests.
174
179
  */
175
180
  export function getEnvApiKey(provider: string): string | undefined {
176
181
  const resolver = serviceProviderMap[provider];
177
182
  if (typeof resolver === "string") {
178
- return $env[resolver];
183
+ return $credentialEnv(resolver);
179
184
  }
180
185
  return resolver?.();
181
186
  }
package/src/types.ts CHANGED
@@ -805,6 +805,10 @@ export interface AnthropicCompat {
805
805
  disableAdaptiveThinking?: boolean;
806
806
  /** Whether tools may include Anthropic's per-tool eager_input_streaming flag. Default: true. */
807
807
  supportsEagerToolInputStreaming?: boolean;
808
+ /** Whether the provider accepts the `tool_choice` parameter at all. Default: true. */
809
+ supportsToolChoice?: boolean;
810
+ /** Whether `tool_choice` may force a tool (`any` / named `tool`). Default: true except known incompatible Anthropic models. */
811
+ supportsForcedToolChoice?: boolean;
808
812
  /** Whether long prompt-cache retention (`ttl: "1h"`) is supported. Default: true for canonical Anthropic API. */
809
813
  supportsLongCacheRetention?: boolean;
810
814
  }