@gajae-code/ai 0.4.4 → 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 +18 -0
- package/dist/types/stream.d.ts +2 -2
- package/dist/types/types.d.ts +4 -0
- package/package.json +2 -2
- package/src/auth-storage.ts +5 -1
- package/src/model-manager.ts +33 -1
- package/src/providers/anthropic.ts +27 -1
- package/src/providers/cursor.ts +70 -1
- package/src/providers/google-gemini-headers.ts +1 -1
- package/src/stream.ts +24 -19
- package/src/types.ts +4 -0
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
|
package/dist/types/stream.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
9
|
-
*
|
|
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
|
/**
|
package/dist/types/types.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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"
|
package/src/auth-storage.ts
CHANGED
|
@@ -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
|
package/src/model-manager.ts
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/src/providers/cursor.ts
CHANGED
|
@@ -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.
|
|
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 { $
|
|
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 = $
|
|
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: () => $
|
|
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": () => $
|
|
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
|
-
? $
|
|
115
|
-
: $
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
!!$
|
|
138
|
-
|
|
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
|
-
|
|
141
|
-
(
|
|
142
|
-
|
|
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: () => $
|
|
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": () => $
|
|
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
|
-
*
|
|
173
|
-
*
|
|
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 $
|
|
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
|
}
|