@gajae-code/ai 0.15.2 → 0.15.3
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 +17 -0
- package/README.md +1 -1
- package/dist/types/auth-storage.d.ts +2 -0
- package/dist/types/model-retirements.d.ts +1 -1
- package/dist/types/stream.d.ts +1 -0
- package/dist/types/types.d.ts +10 -5
- package/dist/types/utils/json-parse.d.ts +9 -1
- package/dist/types/utils/oauth/vllm.d.ts +2 -2
- package/package.json +3 -3
- package/src/auth-storage.ts +30 -0
- package/src/model-retirements.d.ts +1 -1
- package/src/model-retirements.ts +9 -3
- package/src/models.json +1 -76
- package/src/providers/anthropic.ts +16 -1
- package/src/providers/openai-anthropic-shim.ts +1 -0
- package/src/providers/openai-completions.ts +8 -1
- package/src/stream.d.ts +1 -0
- package/src/stream.ts +42 -8
- package/src/types.d.ts +10 -5
- package/src/types.ts +11 -5
- package/src/utils/json-parse.d.ts +9 -1
- package/src/utils/json-parse.ts +47 -2
- package/src/utils/oauth/vllm.ts +10 -8
- package/src/utils/overflow.ts +63 -1
- package/src/utils/tool-call-healing.ts +6 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.15.3] - 2026-08-27
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Valid JSON `\uXXXX` tool arguments now execute as their canonical decoded strings instead of entering the escaped-non-ASCII resample loop. Provider adapters retain guard metadata only for malformed JSON, duplicate/deep evidence, and unpaired UTF-16 surrogates, so those cases remain fail-closed while standard escaped Hangul, emoji surrogate pairs, and printable ASCII no longer consume retries or terminate managed runs.
|
|
10
|
+
- Explicit positive `maxTokens` values declared for custom `models.yml` models
|
|
11
|
+
and model overrides now reach the provider request across the shared stream
|
|
12
|
+
mapping (`max_tokens`, `max_completion_tokens`, and `max_output_tokens`).
|
|
13
|
+
Built-in and discovered metadata retain the conservative 32,000-token default,
|
|
14
|
+
while positive per-request overrides keep precedence; `compat.extraBody` is
|
|
15
|
+
not required to alter the wire budget.
|
|
16
|
+
- `/login vllm` no longer stores the `vllm-local` no-auth sentinel as a persisted API key. `AuthStorage.getApiKey()` resolves stored `api_key` credentials before environment variables, so a stored sentinel from an empty login could outrank a real `VLLM_API_KEY` env var and reach normal inference. `/login vllm` now requires a real API key (empty input throws `vLLM API key is required; local no-auth servers are discovered automatically`); local no-auth servers remain discovered automatically via the descriptor's `allowUnauthenticated` flag and need no login.
|
|
17
|
+
- `classifyContextOverflow` no longer misclassifies Anthropic context overflow as a terminal request error. Anthropic reports overflow inside a generic `invalid_request_error` envelope (`{"type":"invalid_request_error","message":"prompt is too long: 1158066 tokens > 1000000 maximum"}`), and that envelope type was listed as an authoritative non-overflow code, so it vetoed classification before the `prompt is too long` match could run. Overflow was then treated as a terminal 4xx and auto-compaction never ran. The envelope is now separated from cause-naming codes (auth, quota, rate limit), which keep full veto authority. To preserve the existing hostile-prose defense, only Anthropic's self-verifying measured form (`<used> tokens > <limit> maximum`, where the reported usage actually exceeds the reported maximum) can override the envelope; loose prose such as `too many tokens` still cannot flip a typed transport classification.
|
|
18
|
+
- The OpenAI↔Anthropic compatibility shim (Kimi Code, Synthetic) no longer drops the model's `thinking` metadata when routing an OpenAI-compatible model through the Anthropic Messages API. The shim rebuilt the Anthropic model descriptor field-by-field and omitted `thinking`, so a reasoning-capable model such as `kimi-code/k3` failed every effort-qualified request with `Model kimi-code/k3 is missing thinking metadata` (`getSupportedEfforts` throws when a reasoning model lacks thinking metadata). The descriptor now copies `model.thinking` through, restoring effort validation and the effort→`budget_tokens` mapping for the Anthropic-format route.
|
|
19
|
+
|
|
5
20
|
## [0.15.2] - 2026-08-25
|
|
6
21
|
|
|
7
22
|
### Changed
|
|
@@ -17,7 +32,9 @@
|
|
|
17
32
|
|
|
18
33
|
### Fixed
|
|
19
34
|
|
|
35
|
+
- Exact ID-selected stored literal API keys can now authorize existing model-cache provenance without resolving or exposing secret bytes; command-backed rows, stored environment references, OAuth, runtime overrides, and config overrides remain ineligible, while an explicitly selected stored row keeps precedence over provider environment fallback.
|
|
20
36
|
- Tool-call parsing now preserves bounded, payload-free raw `\uXXXX` position/scalar evidence across provider adapters. Printable ASCII escapes are included because a one-nibble mutation can move an intended non-ASCII scalar into ASCII before decoded-value validation. Process-keyed scalar/path tags, raw and decoded offsets, value ordinals, duplicate-key/depth rejection, total-position accounting, and a process-local integrity tag make partial, altered, malformed, or overflowed evidence explicitly fail-closed without retaining recoverable argument characters or field names (#4927).
|
|
37
|
+
- Hid the non-callable `google-antigravity/gemini-3.7-flash-{low,medium,high}` selectors from bundled, dynamic, and cached catalogs after live Cloud Code Assist calls returned HTTP 404; `google-antigravity/gemini-3.7-flash-tiered` remains available and callable.
|
|
21
38
|
- Ollama Cloud discovery now keeps curated output limits authoritative and gives unknown models a bounded 32,000-token fallback capped by their discovered context window. This replaces the truncation-prone 8,192 fallback without treating the server context length as a verified 131,072-token output capability or sending unbounded `num_predict` requests. Hosted cold starts and long prefills also receive a 300-second first-event window while explicit timeout overrides keep precedence (#4921).
|
|
22
39
|
- Direct model selections now retry zero-token empty OpenAI-compatible responses.
|
|
23
40
|
- OpenAI-compatible chat streams now replay an exact `finish_reason: "network_error"` only when no text, reasoning, refusal, or tool-call delta has been exposed. Retries honor `streamMaxRetries`, exponential backoff, caller cancellation, and managed-fallback ownership; failed-attempt usage, cost, response IDs, and partial chunks are discarded while terminal error wording remains compatible (#4918).
|
package/README.md
CHANGED
|
@@ -1074,7 +1074,7 @@ The quickest way to authenticate:
|
|
|
1074
1074
|
```bash
|
|
1075
1075
|
bunx @gajae-code/ai login # interactive provider selection
|
|
1076
1076
|
bunx @gajae-code/ai login anthropic # login to specific provider
|
|
1077
|
-
bunx @gajae-code/ai login vllm # store vLLM API key (
|
|
1077
|
+
bunx @gajae-code/ai login vllm # store vLLM API key (local no-auth servers are discovered automatically)
|
|
1078
1078
|
bunx @gajae-code/ai login xai # sign in with xAI/Grok OAuth
|
|
1079
1079
|
bunx @gajae-code/ai list # list available providers
|
|
1080
1080
|
```
|
|
@@ -580,6 +580,8 @@ export declare class AuthStorage {
|
|
|
580
580
|
hasSessionCredentialAuto(provider: string, scopeId?: string): boolean;
|
|
581
581
|
/** Resolve the effective selector precedence for a provider/scope. */
|
|
582
582
|
resolveEffectiveCredentialSelector(provider: string, scopeId?: string, explicitSelector?: AuthCredentialSelector): AuthCredentialSelector | undefined;
|
|
583
|
+
/** @internal Return cache provenance for an exact stored literal API-key row without resolving its value. */
|
|
584
|
+
getStoredLiteralApiKeyEvidenceGeneration(provider: string, selector: AuthCredentialSelector): string | undefined;
|
|
583
585
|
/** Validate and canonicalize an OAuth-only selector for account pinning. */
|
|
584
586
|
resolveOAuthPinTarget(provider: string, selector: AuthCredentialSelector): OAuthPinTarget;
|
|
585
587
|
/** Return all local inventory rows, including soft-disabled metadata, without payloads. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const RETIRED_MODEL_KEYS: readonly ["google-antigravity/gemini-3.1-pro-high"];
|
|
1
|
+
export declare const RETIRED_MODEL_KEYS: readonly ["google-antigravity/gemini-3.1-pro-high", "google-antigravity/gemini-3.7-flash-high", "google-antigravity/gemini-3.7-flash-low", "google-antigravity/gemini-3.7-flash-medium"];
|
|
2
2
|
export declare function isRetiredModelKey(provider: string, modelId: string): boolean;
|
|
3
3
|
export declare function isRetiredModel(model: {
|
|
4
4
|
provider: string;
|
package/dist/types/stream.d.ts
CHANGED
|
@@ -41,3 +41,4 @@ export declare function completeSimple<TApi extends Api>(model: Model<TApi>, con
|
|
|
41
41
|
export declare const OUTPUT_FALLBACK_BUFFER = 4000;
|
|
42
42
|
export declare const ANTHROPIC_THINKING: Record<Effort, number>;
|
|
43
43
|
export declare function mapAnthropicToolChoice(choice?: ToolChoice): AnthropicOptions["toolChoice"];
|
|
44
|
+
export declare function resolveDefaultRequestMaxTokens<TApi extends Api>(model: Model<TApi>, requested?: number): number;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -405,11 +405,12 @@ export interface ToolCall {
|
|
|
405
405
|
* Such a payload parses cleanly but
|
|
406
406
|
* is unverifiable: one mistyped hex digit decodes to a different, equally
|
|
407
407
|
* valid character, so the text can be silently wrong with no in-band evidence.
|
|
408
|
-
* The agent loop resamples the turn
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
408
|
+
* The agent loop resamples the turn a bounded number of times and then
|
|
409
|
+
* rejects the call instead of executing it. The single bounded exception
|
|
410
|
+
* is a tool that enumerated its display-only fields
|
|
411
|
+
* (`displaySafeEscapedArgFields`): when every escaped scalar corroborates
|
|
412
|
+
* a decoded non-ASCII character inside those fields, the call executes
|
|
413
|
+
* with a warning instead — rendered question text, never executable
|
|
413
414
|
* content, ids, or durable metadata.
|
|
414
415
|
* Escapes that are required (control characters) or unavoidable (lone
|
|
415
416
|
* surrogates) never set this.
|
|
@@ -972,6 +973,8 @@ export interface ModelRequestTransform {
|
|
|
972
973
|
/** Extra request body fields merged after provider defaults; protected core request keys are ignored. */
|
|
973
974
|
extraBody?: Record<string, unknown>;
|
|
974
975
|
}
|
|
976
|
+
/** Provenance used when resolving a model's default request output budget. */
|
|
977
|
+
export type ModelMaxTokensSource = "configured" | "discovered";
|
|
975
978
|
export interface ModelCost {
|
|
976
979
|
input: number;
|
|
977
980
|
output: number;
|
|
@@ -1006,6 +1009,8 @@ export interface Model<TApi extends Api = any> {
|
|
|
1006
1009
|
premiumMultiplier?: number;
|
|
1007
1010
|
contextWindow: number;
|
|
1008
1011
|
maxTokens: number;
|
|
1012
|
+
/** Explicit models.yml/model-override authority; absent keeps the safe transport default. */
|
|
1013
|
+
maxTokensSource?: ModelMaxTokensSource;
|
|
1009
1014
|
headers?: Record<string, string>;
|
|
1010
1015
|
/**
|
|
1011
1016
|
* Streaming transport override. When `"pi-native"`, `streamSimple` routes
|
|
@@ -64,7 +64,15 @@ export declare function findUnnecessaryUnicodeEscape(json: string): string | und
|
|
|
64
64
|
* intentionally omitted to preserve `questions.question`-style field matching.
|
|
65
65
|
*/
|
|
66
66
|
export declare function collectUnicodeEscapeEvidence(json: string): UnicodeEscapeEvidence | undefined;
|
|
67
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Return evidence only when decoded tool arguments are unsafe to execute.
|
|
69
|
+
*
|
|
70
|
+
* Valid JSON escapes and literal UTF-8 have the same canonical decoded value.
|
|
71
|
+
* Malformed JSON, duplicate/deep evidence, and unpaired UTF-16 surrogates keep
|
|
72
|
+
* the existing fail-closed path.
|
|
73
|
+
*/
|
|
74
|
+
export declare function collectUnsafeUnicodeEscapeEvidence(json: string): UnicodeEscapeEvidence | undefined;
|
|
75
|
+
/** Attach unsafe raw evidence while preserving the existing call-level guard flag. */
|
|
68
76
|
export declare function captureUnicodeEscapeEvidence(target: UnicodeEscapeEvidenceTarget, json: string): boolean;
|
|
69
77
|
/** Attach evidence as transient, non-enumerable metadata excluded from serialization. */
|
|
70
78
|
export declare function attachUnicodeEscapeEvidence(target: UnicodeEscapeEvidenceTarget, evidence: UnicodeEscapeEvidence): void;
|
|
@@ -10,7 +10,7 @@ import type { OAuthController } from "./types";
|
|
|
10
10
|
/**
|
|
11
11
|
* Login to vLLM.
|
|
12
12
|
*
|
|
13
|
-
* Opens vLLM OpenAI-compatible auth docs, prompts for
|
|
14
|
-
* and returns a stored key value.
|
|
13
|
+
* Opens vLLM OpenAI-compatible auth docs, prompts for a bearer token,
|
|
14
|
+
* and returns a stored key value. Local no-auth servers need no login.
|
|
15
15
|
*/
|
|
16
16
|
export declare function loginVllm(options: OAuthController): Promise<string>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/ai",
|
|
4
|
-
"version": "0.15.
|
|
4
|
+
"version": "0.15.3",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://gajae-code.com",
|
|
7
7
|
"author": "Yeachan-Heo and Gajae Code Contributors",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@anthropic-ai/sdk": "^0.94.0",
|
|
42
42
|
"@bufbuild/protobuf": "^2.12.0",
|
|
43
|
-
"@gajae-code/natives": "0.15.
|
|
44
|
-
"@gajae-code/utils": "0.15.
|
|
43
|
+
"@gajae-code/natives": "0.15.3",
|
|
44
|
+
"@gajae-code/utils": "0.15.3",
|
|
45
45
|
"openai": "^6.36.0",
|
|
46
46
|
"partial-json": "^0.1.7",
|
|
47
47
|
"zod": "4.4.3"
|
package/src/auth-storage.ts
CHANGED
|
@@ -1359,6 +1359,24 @@ export class AuthStorage {
|
|
|
1359
1359
|
getProviderEvidenceGeneration(provider: string, resolvedApiKey?: string): string {
|
|
1360
1360
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
1361
1361
|
const evidenceApiKey = resolvedApiKey;
|
|
1362
|
+
const storedLiteral =
|
|
1363
|
+
evidenceApiKey === undefined ||
|
|
1364
|
+
this.#runtimeOverrides.has(storageProvider) ||
|
|
1365
|
+
this.#configOverrides.has(storageProvider)
|
|
1366
|
+
? undefined
|
|
1367
|
+
: this.#getCredentialsForProvider(storageProvider).find(
|
|
1368
|
+
(credential): credential is Extract<AuthCredential, { type: "api_key" }> =>
|
|
1369
|
+
credential.type === "api_key" &&
|
|
1370
|
+
credential.key === evidenceApiKey &&
|
|
1371
|
+
!credential.key.startsWith("!") &&
|
|
1372
|
+
process.env[credential.key] === undefined,
|
|
1373
|
+
);
|
|
1374
|
+
if (storedLiteral) {
|
|
1375
|
+
return crypto
|
|
1376
|
+
.createHash("sha256")
|
|
1377
|
+
.update(`stored-literal\u0000${storageProvider}\u0000${storedLiteral.key}`)
|
|
1378
|
+
.digest("hex");
|
|
1379
|
+
}
|
|
1362
1380
|
let selectedCredential: ({ index: number } & StoredCredential) | undefined;
|
|
1363
1381
|
try {
|
|
1364
1382
|
selectedCredential = this.#resolveSelectedStoredCredential(provider, undefined, undefined);
|
|
@@ -1588,6 +1606,18 @@ export class AuthStorage {
|
|
|
1588
1606
|
);
|
|
1589
1607
|
}
|
|
1590
1608
|
|
|
1609
|
+
/** @internal Return cache provenance for an exact stored literal API-key row without resolving its value. */
|
|
1610
|
+
getStoredLiteralApiKeyEvidenceGeneration(provider: string, selector: AuthCredentialSelector): string | undefined {
|
|
1611
|
+
if (selector.kind !== "id") return undefined;
|
|
1612
|
+
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
1613
|
+
if (this.#runtimeOverrides.has(storageProvider) || this.#configOverrides.has(storageProvider)) return undefined;
|
|
1614
|
+
const selected = this.#findCredentialBySelector(storageProvider, selector);
|
|
1615
|
+
if (selected?.credential.type !== "api_key") return undefined;
|
|
1616
|
+
const key = selected.credential.key;
|
|
1617
|
+
if (!key || key.startsWith("!") || process.env[key] !== undefined) return undefined;
|
|
1618
|
+
return this.getProviderEvidenceGeneration(storageProvider, key);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1591
1621
|
/** Validate and canonicalize an OAuth-only selector for account pinning. */
|
|
1592
1622
|
resolveOAuthPinTarget(provider: string, selector: AuthCredentialSelector): OAuthPinTarget {
|
|
1593
1623
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const RETIRED_MODEL_KEYS: readonly ["google-antigravity/gemini-3.1-pro-high"];
|
|
1
|
+
export declare const RETIRED_MODEL_KEYS: readonly ["google-antigravity/gemini-3.1-pro-high", "google-antigravity/gemini-3.7-flash-high", "google-antigravity/gemini-3.7-flash-low", "google-antigravity/gemini-3.7-flash-medium"];
|
|
2
2
|
export declare function isRetiredModelKey(provider: string, modelId: string): boolean;
|
|
3
3
|
export declare function isRetiredModel(model: {
|
|
4
4
|
provider: string;
|
package/src/model-retirements.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
// Retired from advertised catalogs because Cloud Code Assist rejects live calls
|
|
2
|
-
//
|
|
3
|
-
|
|
1
|
+
// Retired from advertised catalogs because Cloud Code Assist rejects live calls.
|
|
2
|
+
// Keep the callable alternatives visible: gemini-3.1-pro-low:high and
|
|
3
|
+
// gemini-3.7-flash-tiered.
|
|
4
|
+
export const RETIRED_MODEL_KEYS = [
|
|
5
|
+
"google-antigravity/gemini-3.1-pro-high",
|
|
6
|
+
"google-antigravity/gemini-3.7-flash-high",
|
|
7
|
+
"google-antigravity/gemini-3.7-flash-low",
|
|
8
|
+
"google-antigravity/gemini-3.7-flash-medium",
|
|
9
|
+
] as const;
|
|
4
10
|
|
|
5
11
|
const RETIRED_MODEL_KEY_SET = new Set<string>(RETIRED_MODEL_KEYS);
|
|
6
12
|
|
package/src/models.json
CHANGED
|
@@ -15785,31 +15785,6 @@
|
|
|
15785
15785
|
"maxLevel": "high"
|
|
15786
15786
|
}
|
|
15787
15787
|
},
|
|
15788
|
-
"gemini-3.7-flash-high": {
|
|
15789
|
-
"id": "gemini-3.7-flash-high",
|
|
15790
|
-
"name": "Gemini 3.7 Flash (High) (Antigravity)",
|
|
15791
|
-
"api": "google-gemini-cli",
|
|
15792
|
-
"provider": "google-antigravity",
|
|
15793
|
-
"baseUrl": "https://daily-cloudcode-pa.sandbox.googleapis.com",
|
|
15794
|
-
"reasoning": true,
|
|
15795
|
-
"input": [
|
|
15796
|
-
"text",
|
|
15797
|
-
"image"
|
|
15798
|
-
],
|
|
15799
|
-
"cost": {
|
|
15800
|
-
"input": 0,
|
|
15801
|
-
"output": 0,
|
|
15802
|
-
"cacheRead": 0,
|
|
15803
|
-
"cacheWrite": 0
|
|
15804
|
-
},
|
|
15805
|
-
"contextWindow": 1048576,
|
|
15806
|
-
"maxTokens": 65536,
|
|
15807
|
-
"thinking": {
|
|
15808
|
-
"mode": "google-level",
|
|
15809
|
-
"minLevel": "low",
|
|
15810
|
-
"maxLevel": "high"
|
|
15811
|
-
}
|
|
15812
|
-
},
|
|
15813
15788
|
"gemini-3.6-flash-low": {
|
|
15814
15789
|
"id": "gemini-3.6-flash-low",
|
|
15815
15790
|
"name": "Gemini 3.6 Flash (Low) (Antigravity)",
|
|
@@ -15835,31 +15810,6 @@
|
|
|
15835
15810
|
"maxLevel": "high"
|
|
15836
15811
|
}
|
|
15837
15812
|
},
|
|
15838
|
-
"gemini-3.7-flash-low": {
|
|
15839
|
-
"id": "gemini-3.7-flash-low",
|
|
15840
|
-
"name": "Gemini 3.7 Flash (Low) (Antigravity)",
|
|
15841
|
-
"api": "google-gemini-cli",
|
|
15842
|
-
"provider": "google-antigravity",
|
|
15843
|
-
"baseUrl": "https://daily-cloudcode-pa.sandbox.googleapis.com",
|
|
15844
|
-
"reasoning": true,
|
|
15845
|
-
"input": [
|
|
15846
|
-
"text",
|
|
15847
|
-
"image"
|
|
15848
|
-
],
|
|
15849
|
-
"cost": {
|
|
15850
|
-
"input": 0,
|
|
15851
|
-
"output": 0,
|
|
15852
|
-
"cacheRead": 0,
|
|
15853
|
-
"cacheWrite": 0
|
|
15854
|
-
},
|
|
15855
|
-
"contextWindow": 1048576,
|
|
15856
|
-
"maxTokens": 65536,
|
|
15857
|
-
"thinking": {
|
|
15858
|
-
"mode": "google-level",
|
|
15859
|
-
"minLevel": "low",
|
|
15860
|
-
"maxLevel": "high"
|
|
15861
|
-
}
|
|
15862
|
-
},
|
|
15863
15813
|
"gemini-3.6-flash-medium": {
|
|
15864
15814
|
"id": "gemini-3.6-flash-medium",
|
|
15865
15815
|
"name": "Gemini 3.6 Flash (Medium) (Antigravity)",
|
|
@@ -15885,31 +15835,6 @@
|
|
|
15885
15835
|
"maxLevel": "high"
|
|
15886
15836
|
}
|
|
15887
15837
|
},
|
|
15888
|
-
"gemini-3.7-flash-medium": {
|
|
15889
|
-
"id": "gemini-3.7-flash-medium",
|
|
15890
|
-
"name": "Gemini 3.7 Flash (Medium) (Antigravity)",
|
|
15891
|
-
"api": "google-gemini-cli",
|
|
15892
|
-
"provider": "google-antigravity",
|
|
15893
|
-
"baseUrl": "https://daily-cloudcode-pa.sandbox.googleapis.com",
|
|
15894
|
-
"reasoning": true,
|
|
15895
|
-
"input": [
|
|
15896
|
-
"text",
|
|
15897
|
-
"image"
|
|
15898
|
-
],
|
|
15899
|
-
"cost": {
|
|
15900
|
-
"input": 0,
|
|
15901
|
-
"output": 0,
|
|
15902
|
-
"cacheRead": 0,
|
|
15903
|
-
"cacheWrite": 0
|
|
15904
|
-
},
|
|
15905
|
-
"contextWindow": 1048576,
|
|
15906
|
-
"maxTokens": 65536,
|
|
15907
|
-
"thinking": {
|
|
15908
|
-
"mode": "google-level",
|
|
15909
|
-
"minLevel": "low",
|
|
15910
|
-
"maxLevel": "high"
|
|
15911
|
-
}
|
|
15912
|
-
},
|
|
15913
15838
|
"gemini-3.6-flash-tiered": {
|
|
15914
15839
|
"id": "gemini-3.6-flash-tiered",
|
|
15915
15840
|
"name": "gemini-3.6-flash-tiered",
|
|
@@ -93690,4 +93615,4 @@
|
|
|
93690
93615
|
}
|
|
93691
93616
|
}
|
|
93692
93617
|
}
|
|
93693
|
-
}
|
|
93618
|
+
}
|
|
@@ -3320,7 +3320,22 @@ function ensureMaxTokensForThinking(params: MessageCreateParamsStreaming, model:
|
|
|
3320
3320
|
const maxTokens = params.max_tokens ?? 0;
|
|
3321
3321
|
const requiredMaxTokens = budgetTokens + OUTPUT_FALLBACK_BUFFER;
|
|
3322
3322
|
if (maxTokens < requiredMaxTokens) {
|
|
3323
|
-
|
|
3323
|
+
const modelMaxTokens =
|
|
3324
|
+
Number.isSafeInteger(model.maxTokens) && model.maxTokens > 0
|
|
3325
|
+
? model.maxTokens
|
|
3326
|
+
: Math.max(maxTokens, requiredMaxTokens);
|
|
3327
|
+
params.max_tokens = Math.min(requiredMaxTokens, modelMaxTokens);
|
|
3328
|
+
}
|
|
3329
|
+
// Anthropic requires budget_tokens strictly below max_tokens; when the cap
|
|
3330
|
+
// cannot fit the requested budget plus the output buffer, shrink the budget
|
|
3331
|
+
// (or disable thinking) instead of sending an invalid pair.
|
|
3332
|
+
const cappedBudget = params.max_tokens - OUTPUT_FALLBACK_BUFFER;
|
|
3333
|
+
if (cappedBudget < budgetTokens) {
|
|
3334
|
+
if (cappedBudget <= 0) {
|
|
3335
|
+
params.thinking = { type: "disabled" };
|
|
3336
|
+
} else {
|
|
3337
|
+
params.thinking = { type: "enabled", budget_tokens: cappedBudget };
|
|
3338
|
+
}
|
|
3324
3339
|
}
|
|
3325
3340
|
}
|
|
3326
3341
|
|
|
@@ -1637,7 +1637,14 @@ function buildParams(
|
|
|
1637
1637
|
}
|
|
1638
1638
|
|
|
1639
1639
|
if (compat.extraBody) {
|
|
1640
|
-
|
|
1640
|
+
// The resolved output limit owns the selected wire field; extraBody is a
|
|
1641
|
+
// free-form compatibility escape hatch and must not add a competing
|
|
1642
|
+
// max-token field or overwrite the resolved budget.
|
|
1643
|
+
const { max_tokens, max_completion_tokens, max_output_tokens, ...restExtra } = compat.extraBody as Record<
|
|
1644
|
+
string,
|
|
1645
|
+
unknown
|
|
1646
|
+
>;
|
|
1647
|
+
Object.assign(params, restExtra);
|
|
1641
1648
|
}
|
|
1642
1649
|
applyOpenAIRequestTransformBody(params, model.requestTransform);
|
|
1643
1650
|
if (!supportsReasoningParams) {
|
package/src/stream.d.ts
CHANGED
|
@@ -41,3 +41,4 @@ export declare function completeSimple<TApi extends Api>(model: Model<TApi>, con
|
|
|
41
41
|
export declare const OUTPUT_FALLBACK_BUFFER = 4000;
|
|
42
42
|
export declare const ANTHROPIC_THINKING: Record<Effort, number>;
|
|
43
43
|
export declare function mapAnthropicToolChoice(choice?: ToolChoice): AnthropicOptions["toolChoice"];
|
|
44
|
+
export declare function resolveDefaultRequestMaxTokens<TApi extends Api>(model: Model<TApi>, requested?: number): number;
|
package/src/stream.ts
CHANGED
|
@@ -320,6 +320,14 @@ export function stream<TApi extends Api>(
|
|
|
320
320
|
if (options?.fallbackManaged) {
|
|
321
321
|
options = { ...options, requestMaxRetries: 0, streamMaxRetries: 0 } as OptionsForApi<TApi>;
|
|
322
322
|
}
|
|
323
|
+
// Canonical low-level boundary: the request budget must be a positive safe
|
|
324
|
+
// integer. Provider options arrive here unvalidated (unlike `streamSimple`,
|
|
325
|
+
// whose resolver already normalizes), so an unsafe value is dropped to
|
|
326
|
+
// unspecified here once for every dispatch below — integer-only provider
|
|
327
|
+
// fields can never receive a fractional or MAX_SAFE_INTEGER+1 budget.
|
|
328
|
+
if (options?.maxTokens !== undefined && !(Number.isSafeInteger(options.maxTokens) && options.maxTokens > 0)) {
|
|
329
|
+
options = { ...options, maxTokens: undefined } as OptionsForApi<TApi>;
|
|
330
|
+
}
|
|
323
331
|
// Check custom API registry first (extension-provided APIs like "vertex-Anthropic model-api")
|
|
324
332
|
const customApiProvider = getCustomApi(model.api);
|
|
325
333
|
if (customApiProvider) {
|
|
@@ -613,17 +621,18 @@ export function streamSimple<TApi extends Api>(
|
|
|
613
621
|
// the gateway bearer instead. Comes BEFORE the custom-API check so
|
|
614
622
|
// extension-registered APIs can't accidentally override a configured
|
|
615
623
|
// pi-native transport.
|
|
624
|
+
const resolvedRequestMaxTokens = resolveDefaultRequestMaxTokens(model, options?.maxTokens);
|
|
616
625
|
if (model.transport === "pi-native") {
|
|
617
626
|
return streamFromLazyImport(async () => {
|
|
618
627
|
const { streamPiNative } = await import("./providers/pi-native-client");
|
|
619
|
-
return streamPiNative(model, context, options);
|
|
628
|
+
return streamPiNative(model, context, { ...options, maxTokens: resolvedRequestMaxTokens });
|
|
620
629
|
}, options?.signal);
|
|
621
630
|
}
|
|
622
631
|
|
|
623
632
|
// Check custom API registry (extension-provided APIs)
|
|
624
633
|
const customApiProvider = getCustomApi(model.api);
|
|
625
634
|
if (customApiProvider) {
|
|
626
|
-
return customApiProvider.streamSimple(model, context, options);
|
|
635
|
+
return customApiProvider.streamSimple(model, context, { ...options, maxTokens: resolvedRequestMaxTokens });
|
|
627
636
|
}
|
|
628
637
|
|
|
629
638
|
// Vertex AI uses Application Default Credentials, not API keys
|
|
@@ -643,6 +652,7 @@ export function streamSimple<TApi extends Api>(
|
|
|
643
652
|
const adapterOptions = isProviderSafetyStopModelTrusted(model)
|
|
644
653
|
? withProviderSafetyStopAdapterInvocation(options ?? {})
|
|
645
654
|
: options;
|
|
655
|
+
const resolvedSpecialProviderMaxTokens = resolvedRequestMaxTokens;
|
|
646
656
|
|
|
647
657
|
// GitLab Duo - wraps Anthropic/OpenAI behind GitLab AI Gateway direct access tokens
|
|
648
658
|
if (model.provider === "gitlab-duo") {
|
|
@@ -654,6 +664,7 @@ export function streamSimple<TApi extends Api>(
|
|
|
654
664
|
copyProviderSafetyStopAdapterInvocation(adapterOptions, {
|
|
655
665
|
...adapterOptions,
|
|
656
666
|
apiKey,
|
|
667
|
+
maxTokens: resolvedSpecialProviderMaxTokens,
|
|
657
668
|
}),
|
|
658
669
|
);
|
|
659
670
|
}, options?.signal);
|
|
@@ -670,6 +681,7 @@ export function streamSimple<TApi extends Api>(
|
|
|
670
681
|
copyProviderSafetyStopAdapterInvocation(adapterOptions, {
|
|
671
682
|
...adapterOptions,
|
|
672
683
|
apiKey,
|
|
684
|
+
maxTokens: resolvedSpecialProviderMaxTokens,
|
|
673
685
|
format: options?.kimiApiFormat ?? "anthropic",
|
|
674
686
|
}),
|
|
675
687
|
);
|
|
@@ -687,6 +699,7 @@ export function streamSimple<TApi extends Api>(
|
|
|
687
699
|
copyProviderSafetyStopAdapterInvocation(adapterOptions, {
|
|
688
700
|
...adapterOptions,
|
|
689
701
|
apiKey,
|
|
702
|
+
maxTokens: resolvedSpecialProviderMaxTokens,
|
|
690
703
|
format: options?.syntheticApiFormat ?? "openai", // Default to OpenAI format
|
|
691
704
|
}),
|
|
692
705
|
);
|
|
@@ -707,6 +720,7 @@ export async function completeSimple<TApi extends Api>(
|
|
|
707
720
|
}
|
|
708
721
|
|
|
709
722
|
const MIN_OUTPUT_TOKENS = 1024;
|
|
723
|
+
const DEFAULT_REQUEST_MAX_TOKENS = 32000;
|
|
710
724
|
export const OUTPUT_FALLBACK_BUFFER = 4000;
|
|
711
725
|
const ANTHROPIC_USE_INTERLEAVED_THINKING = Bun.env.PI_NO_INTERLEAVED_THINKING !== "1";
|
|
712
726
|
|
|
@@ -804,6 +818,16 @@ function resolveOpenAiReasoningEffort<TApi extends Api>(
|
|
|
804
818
|
|
|
805
819
|
const castApi = <TApi extends Api>(api: OptionsForApi<TApi>): OptionsForApi<Api> => api as OptionsForApi<Api>;
|
|
806
820
|
|
|
821
|
+
export function resolveDefaultRequestMaxTokens<TApi extends Api>(model: Model<TApi>, requested?: number): number {
|
|
822
|
+
if (requested !== undefined && Number.isSafeInteger(requested) && requested > 0) return requested;
|
|
823
|
+
if (model.maxTokensSource === "configured" && Number.isSafeInteger(model.maxTokens) && model.maxTokens > 0) {
|
|
824
|
+
return model.maxTokens;
|
|
825
|
+
}
|
|
826
|
+
return Number.isSafeInteger(model.maxTokens) && model.maxTokens > 0
|
|
827
|
+
? Math.min(model.maxTokens, DEFAULT_REQUEST_MAX_TOKENS)
|
|
828
|
+
: DEFAULT_REQUEST_MAX_TOKENS;
|
|
829
|
+
}
|
|
830
|
+
|
|
807
831
|
function mapOptionsForApi<TApi extends Api>(
|
|
808
832
|
model: Model<TApi>,
|
|
809
833
|
options?: SimpleStreamOptions,
|
|
@@ -816,7 +840,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
816
840
|
minP: options?.minP,
|
|
817
841
|
presencePenalty: options?.presencePenalty,
|
|
818
842
|
repetitionPenalty: options?.repetitionPenalty,
|
|
819
|
-
maxTokens:
|
|
843
|
+
maxTokens: resolveDefaultRequestMaxTokens(model, options?.maxTokens),
|
|
820
844
|
signal: options?.signal,
|
|
821
845
|
apiKey: apiKey || options?.apiKey,
|
|
822
846
|
fallbackManaged: options?.fallbackManaged,
|
|
@@ -888,8 +912,13 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
888
912
|
});
|
|
889
913
|
}
|
|
890
914
|
|
|
891
|
-
// Caller's maxTokens is the desired output; add thinking budget on top,
|
|
892
|
-
|
|
915
|
+
// Caller's maxTokens is the desired output; add thinking budget on top,
|
|
916
|
+
// capped at the model limit. `base.maxTokens` is already resolver-sanitized,
|
|
917
|
+
// so only a finite positive model cap participates (malformed metadata
|
|
918
|
+
// cannot reintroduce NaN into the wire budget).
|
|
919
|
+
const modelCap =
|
|
920
|
+
Number.isSafeInteger(model.maxTokens) && model.maxTokens > 0 ? model.maxTokens : base.maxTokens;
|
|
921
|
+
const maxTokens = Math.min((base.maxTokens || 0) + thinkingBudget, modelCap);
|
|
893
922
|
|
|
894
923
|
// If not enough room for thinking + output, reduce thinking budget
|
|
895
924
|
if (maxTokens <= thinkingBudget) {
|
|
@@ -1048,12 +1077,17 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1048
1077
|
|
|
1049
1078
|
let thinkingBudget = options.thinkingBudgets?.[effort] ?? GOOGLE_THINKING[effort];
|
|
1050
1079
|
|
|
1051
|
-
// Caller's maxTokens is the desired output; add thinking budget on top,
|
|
1052
|
-
|
|
1080
|
+
// Caller's maxTokens is the desired output; add thinking budget on top,
|
|
1081
|
+
// capped at the model limit. `base.maxTokens` is already resolver-sanitized,
|
|
1082
|
+
// so only a finite positive model cap participates (malformed metadata
|
|
1083
|
+
// cannot reintroduce NaN into the wire budget).
|
|
1084
|
+
const modelCap =
|
|
1085
|
+
Number.isSafeInteger(model.maxTokens) && model.maxTokens > 0 ? model.maxTokens : base.maxTokens;
|
|
1086
|
+
const maxTokens = Math.min((base.maxTokens || 0) + thinkingBudget, modelCap);
|
|
1053
1087
|
|
|
1054
1088
|
// If not enough room for thinking + output, reduce thinking budget
|
|
1055
1089
|
if (maxTokens <= thinkingBudget) {
|
|
1056
|
-
thinkingBudget = Math.max(0, maxTokens - MIN_OUTPUT_TOKENS)
|
|
1090
|
+
thinkingBudget = Math.max(0, maxTokens - MIN_OUTPUT_TOKENS);
|
|
1057
1091
|
}
|
|
1058
1092
|
|
|
1059
1093
|
// If thinking budget is too low, disable thinking
|
package/src/types.d.ts
CHANGED
|
@@ -405,11 +405,12 @@ export interface ToolCall {
|
|
|
405
405
|
* Such a payload parses cleanly but
|
|
406
406
|
* is unverifiable: one mistyped hex digit decodes to a different, equally
|
|
407
407
|
* valid character, so the text can be silently wrong with no in-band evidence.
|
|
408
|
-
* The agent loop resamples the turn
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
408
|
+
* The agent loop resamples the turn a bounded number of times and then
|
|
409
|
+
* rejects the call instead of executing it. The single bounded exception
|
|
410
|
+
* is a tool that enumerated its display-only fields
|
|
411
|
+
* (`displaySafeEscapedArgFields`): when every escaped scalar corroborates
|
|
412
|
+
* a decoded non-ASCII character inside those fields, the call executes
|
|
413
|
+
* with a warning instead — rendered question text, never executable
|
|
413
414
|
* content, ids, or durable metadata.
|
|
414
415
|
* Escapes that are required (control characters) or unavoidable (lone
|
|
415
416
|
* surrogates) never set this.
|
|
@@ -972,6 +973,8 @@ export interface ModelRequestTransform {
|
|
|
972
973
|
/** Extra request body fields merged after provider defaults; protected core request keys are ignored. */
|
|
973
974
|
extraBody?: Record<string, unknown>;
|
|
974
975
|
}
|
|
976
|
+
/** Provenance used when resolving a model's default request output budget. */
|
|
977
|
+
export type ModelMaxTokensSource = "configured" | "discovered";
|
|
975
978
|
export interface ModelCost {
|
|
976
979
|
input: number;
|
|
977
980
|
output: number;
|
|
@@ -1006,6 +1009,8 @@ export interface Model<TApi extends Api = any> {
|
|
|
1006
1009
|
premiumMultiplier?: number;
|
|
1007
1010
|
contextWindow: number;
|
|
1008
1011
|
maxTokens: number;
|
|
1012
|
+
/** Explicit models.yml/model-override authority; absent keeps the safe transport default. */
|
|
1013
|
+
maxTokensSource?: ModelMaxTokensSource;
|
|
1009
1014
|
headers?: Record<string, string>;
|
|
1010
1015
|
/**
|
|
1011
1016
|
* Streaming transport override. When `"pi-native"`, `streamSimple` routes
|
package/src/types.ts
CHANGED
|
@@ -626,11 +626,12 @@ export interface ToolCall {
|
|
|
626
626
|
* Such a payload parses cleanly but
|
|
627
627
|
* is unverifiable: one mistyped hex digit decodes to a different, equally
|
|
628
628
|
* valid character, so the text can be silently wrong with no in-band evidence.
|
|
629
|
-
* The agent loop resamples the turn
|
|
630
|
-
*
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
629
|
+
* The agent loop resamples the turn a bounded number of times and then
|
|
630
|
+
* rejects the call instead of executing it. The single bounded exception
|
|
631
|
+
* is a tool that enumerated its display-only fields
|
|
632
|
+
* (`displaySafeEscapedArgFields`): when every escaped scalar corroborates
|
|
633
|
+
* a decoded non-ASCII character inside those fields, the call executes
|
|
634
|
+
* with a warning instead — rendered question text, never executable
|
|
634
635
|
* content, ids, or durable metadata.
|
|
635
636
|
* Escapes that are required (control characters) or unavoidable (lone
|
|
636
637
|
* surrogates) never set this.
|
|
@@ -1178,6 +1179,9 @@ export interface ModelRequestTransform {
|
|
|
1178
1179
|
extraBody?: Record<string, unknown>;
|
|
1179
1180
|
}
|
|
1180
1181
|
|
|
1182
|
+
/** Provenance used when resolving a model's default request output budget. */
|
|
1183
|
+
export type ModelMaxTokensSource = "configured" | "discovered";
|
|
1184
|
+
|
|
1181
1185
|
export interface ModelCost {
|
|
1182
1186
|
input: number; // $/million tokens
|
|
1183
1187
|
output: number; // $/million tokens
|
|
@@ -1214,6 +1218,8 @@ export interface Model<TApi extends Api = any> {
|
|
|
1214
1218
|
premiumMultiplier?: number;
|
|
1215
1219
|
contextWindow: number;
|
|
1216
1220
|
maxTokens: number;
|
|
1221
|
+
/** Explicit models.yml/model-override authority; absent keeps the safe transport default. */
|
|
1222
|
+
maxTokensSource?: ModelMaxTokensSource;
|
|
1217
1223
|
headers?: Record<string, string>;
|
|
1218
1224
|
/**
|
|
1219
1225
|
* Streaming transport override. When `"pi-native"`, `streamSimple` routes
|
|
@@ -64,7 +64,15 @@ export declare function findUnnecessaryUnicodeEscape(json: string): string | und
|
|
|
64
64
|
* intentionally omitted to preserve `questions.question`-style field matching.
|
|
65
65
|
*/
|
|
66
66
|
export declare function collectUnicodeEscapeEvidence(json: string): UnicodeEscapeEvidence | undefined;
|
|
67
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Return evidence only when decoded tool arguments are unsafe to execute.
|
|
69
|
+
*
|
|
70
|
+
* Valid JSON escapes and literal UTF-8 have the same canonical decoded value.
|
|
71
|
+
* Malformed JSON, duplicate/deep evidence, and unpaired UTF-16 surrogates keep
|
|
72
|
+
* the existing fail-closed path.
|
|
73
|
+
*/
|
|
74
|
+
export declare function collectUnsafeUnicodeEscapeEvidence(json: string): UnicodeEscapeEvidence | undefined;
|
|
75
|
+
/** Attach unsafe raw evidence while preserving the existing call-level guard flag. */
|
|
68
76
|
export declare function captureUnicodeEscapeEvidence(target: UnicodeEscapeEvidenceTarget, json: string): boolean;
|
|
69
77
|
/** Attach evidence as transient, non-enumerable metadata excluded from serialization. */
|
|
70
78
|
export declare function attachUnicodeEscapeEvidence(target: UnicodeEscapeEvidenceTarget, evidence: UnicodeEscapeEvidence): void;
|
package/src/utils/json-parse.ts
CHANGED
|
@@ -484,9 +484,54 @@ export function collectUnicodeEscapeEvidence(json: string): UnicodeEscapeEvidenc
|
|
|
484
484
|
return createUnicodeEscapeEvidence(positions, totalPositions, truncated, false);
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
-
|
|
487
|
+
function hasUnpairedUnicodeSurrogate(value: unknown): boolean {
|
|
488
|
+
const pending: unknown[] = [value];
|
|
489
|
+
const seen = new WeakSet<object>();
|
|
490
|
+
while (pending.length > 0) {
|
|
491
|
+
const current = pending.pop();
|
|
492
|
+
if (typeof current === "string") {
|
|
493
|
+
if (!current.isWellFormed()) return true;
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (typeof current !== "object" || current === null || seen.has(current)) continue;
|
|
497
|
+
seen.add(current);
|
|
498
|
+
if (Array.isArray(current)) {
|
|
499
|
+
for (const child of current) pending.push(child);
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
for (const [key, child] of Object.entries(current)) {
|
|
503
|
+
if (!key.isWellFormed()) return true;
|
|
504
|
+
pending.push(child);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Return evidence only when decoded tool arguments are unsafe to execute.
|
|
512
|
+
*
|
|
513
|
+
* Valid JSON escapes and literal UTF-8 have the same canonical decoded value.
|
|
514
|
+
* Malformed JSON, duplicate/deep evidence, and unpaired UTF-16 surrogates keep
|
|
515
|
+
* the existing fail-closed path.
|
|
516
|
+
*/
|
|
517
|
+
export function collectUnsafeUnicodeEscapeEvidence(json: string): UnicodeEscapeEvidence | undefined {
|
|
518
|
+
const hasUnicodeEscape = json.includes("\\u");
|
|
519
|
+
if (!hasUnicodeEscape && json.isWellFormed()) return undefined;
|
|
520
|
+
const evidence = hasUnicodeEscape ? collectUnicodeEscapeEvidence(json) : undefined;
|
|
521
|
+
if (evidence?.malformed) return evidence;
|
|
522
|
+
try {
|
|
523
|
+
if (hasUnpairedUnicodeSurrogate(JSON.parse(json))) {
|
|
524
|
+
return createUnicodeEscapeEvidence([], 0, false, true);
|
|
525
|
+
}
|
|
526
|
+
} catch {
|
|
527
|
+
return createUnicodeEscapeEvidence([], 0, false, true);
|
|
528
|
+
}
|
|
529
|
+
return undefined;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Attach unsafe raw evidence while preserving the existing call-level guard flag. */
|
|
488
533
|
export function captureUnicodeEscapeEvidence(target: UnicodeEscapeEvidenceTarget, json: string): boolean {
|
|
489
|
-
const evidence =
|
|
534
|
+
const evidence = collectUnsafeUnicodeEscapeEvidence(json);
|
|
490
535
|
if (!evidence) return false;
|
|
491
536
|
attachUnicodeEscapeEvidence(target, evidence);
|
|
492
537
|
return true;
|
package/src/utils/oauth/vllm.ts
CHANGED
|
@@ -12,12 +12,11 @@ import type { OAuthController, OAuthProvider } from "./types";
|
|
|
12
12
|
const PROVIDER_ID: OAuthProvider = "vllm";
|
|
13
13
|
const AUTH_URL = "https://docs.vllm.ai/en/latest/serving/openai_compatible_server.html";
|
|
14
14
|
const DEFAULT_LOCAL_BASE_URL = "http://127.0.0.1:8000/v1";
|
|
15
|
-
const DEFAULT_LOCAL_TOKEN = "vllm-local";
|
|
16
15
|
/**
|
|
17
16
|
* Login to vLLM.
|
|
18
17
|
*
|
|
19
|
-
* Opens vLLM OpenAI-compatible auth docs, prompts for
|
|
20
|
-
* and returns a stored key value.
|
|
18
|
+
* Opens vLLM OpenAI-compatible auth docs, prompts for a bearer token,
|
|
19
|
+
* and returns a stored key value. Local no-auth servers need no login.
|
|
21
20
|
*/
|
|
22
21
|
export async function loginVllm(options: OAuthController): Promise<string> {
|
|
23
22
|
if (!options.onPrompt) {
|
|
@@ -25,16 +24,19 @@ export async function loginVllm(options: OAuthController): Promise<string> {
|
|
|
25
24
|
}
|
|
26
25
|
options.onAuth?.({
|
|
27
26
|
url: AUTH_URL,
|
|
28
|
-
instructions: `Paste
|
|
27
|
+
instructions: `Paste the API key configured with vLLM's --api-key option. Local no-auth servers at ${DEFAULT_LOCAL_BASE_URL} are discovered automatically and do not need /login.`,
|
|
29
28
|
});
|
|
30
29
|
const apiKey = await options.onPrompt({
|
|
31
|
-
message: "Paste your vLLM API key
|
|
32
|
-
placeholder:
|
|
33
|
-
allowEmpty:
|
|
30
|
+
message: "Paste your vLLM API key",
|
|
31
|
+
placeholder: "vLLM API key",
|
|
32
|
+
allowEmpty: false,
|
|
34
33
|
});
|
|
35
34
|
if (options.signal?.aborted) {
|
|
36
35
|
throw new Error("Login cancelled");
|
|
37
36
|
}
|
|
38
37
|
const trimmed = apiKey.trim();
|
|
39
|
-
|
|
38
|
+
if (!trimmed) {
|
|
39
|
+
throw new Error("vLLM API key is required; local no-auth servers are discovered automatically");
|
|
40
|
+
}
|
|
41
|
+
return trimmed;
|
|
40
42
|
}
|
package/src/utils/overflow.ts
CHANGED
|
@@ -127,8 +127,12 @@ const EMPTY_RESPONSE_USAGE_THRESHOLD = 5;
|
|
|
127
127
|
* misleading error text.
|
|
128
128
|
*/
|
|
129
129
|
const OVERFLOW_PROVIDER_CODES = new Set(["context_length_exceeded", "request_too_large"]);
|
|
130
|
+
/**
|
|
131
|
+
* Codes that name a specific non-overflow *cause*. These are authoritative and
|
|
132
|
+
* can never be upgraded by error prose. Generic HTTP envelope types belong in
|
|
133
|
+
* {@link GENERIC_ENVELOPE_PROVIDER_CODES} instead.
|
|
134
|
+
*/
|
|
130
135
|
const NON_OVERFLOW_PROVIDER_CODES = new Set([
|
|
131
|
-
"invalid_request_error",
|
|
132
136
|
"authentication_error",
|
|
133
137
|
"invalid_api_key",
|
|
134
138
|
"invalid_token",
|
|
@@ -158,6 +162,53 @@ function hasTypedNonOverflowCode(transportFailure: TransportFailureFacts | undef
|
|
|
158
162
|
return transportCodes(transportFailure).some(code => NON_OVERFLOW_PROVIDER_CODES.has(code));
|
|
159
163
|
}
|
|
160
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Generic envelope codes that name the HTTP error *category*, not its cause.
|
|
167
|
+
*
|
|
168
|
+
* Anthropic reports context overflow through this envelope:
|
|
169
|
+
*
|
|
170
|
+
* {"type":"error","error":{"type":"invalid_request_error",
|
|
171
|
+
* "message":"prompt is too long: 1158066 tokens > 1000000 maximum"}}
|
|
172
|
+
*
|
|
173
|
+
* Treating the envelope as an authoritative non-overflow cause vetoed the
|
|
174
|
+
* overflow classification, so auto-compaction never ran and the session died on
|
|
175
|
+
* the very overflow it was supposed to absorb.
|
|
176
|
+
*
|
|
177
|
+
* Unlike {@link NON_OVERFLOW_PROVIDER_CODES} (auth, quota, rate limit), this
|
|
178
|
+
* envelope names no cause, so it must not veto an overflow the provider stated
|
|
179
|
+
* quantitatively. It still vetoes free-form prose: only the self-verifying
|
|
180
|
+
* measured form below can override it.
|
|
181
|
+
*/
|
|
182
|
+
const GENERIC_ENVELOPE_PROVIDER_CODES = new Set(["invalid_request_error"]);
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Anthropic's measured overflow report: `<used> tokens > <limit> maximum`.
|
|
186
|
+
*
|
|
187
|
+
* Deliberately far narrower than {@link OVERFLOW_PATTERNS}. Those patterns
|
|
188
|
+
* include loose prose (`too many tokens`, `token limit exceeded`) that a tool
|
|
189
|
+
* result or a model-authored string can trivially contain, so they must never
|
|
190
|
+
* be able to flip a typed transport classification. This form carries its own
|
|
191
|
+
* arithmetic proof and is verified below, so injected text cannot satisfy it
|
|
192
|
+
* without also asserting a real overage.
|
|
193
|
+
*/
|
|
194
|
+
const ANTHROPIC_MEASURED_OVERFLOW_PATTERN = /prompt is too long:\s*(\d+)\s*tokens?\s*>\s*(\d+)\s*maximum/i;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* True only for a provider-measured overflow that verifies against itself:
|
|
198
|
+
* the reported usage must actually exceed the reported maximum.
|
|
199
|
+
*/
|
|
200
|
+
function hasSelfVerifyingOverflowMeasurement(message: AssistantMessage): boolean {
|
|
201
|
+
if (message.stopReason !== "error") return false;
|
|
202
|
+
const errorMessage = message.errorMessage;
|
|
203
|
+
if (!errorMessage) return false;
|
|
204
|
+
const match = ANTHROPIC_MEASURED_OVERFLOW_PATTERN.exec(errorMessage);
|
|
205
|
+
if (!match) return false;
|
|
206
|
+
const used = Number(match[1]);
|
|
207
|
+
const maximum = Number(match[2]);
|
|
208
|
+
if (!Number.isFinite(used) || !Number.isFinite(maximum) || maximum <= 0) return false;
|
|
209
|
+
return used > maximum;
|
|
210
|
+
}
|
|
211
|
+
|
|
161
212
|
function isTypedNoBodyOverflow(
|
|
162
213
|
message: AssistantMessage,
|
|
163
214
|
transportFailure: TransportFailureFacts | undefined,
|
|
@@ -174,7 +225,18 @@ export function classifyContextOverflow(
|
|
|
174
225
|
if (transportFailure?.status === 429) return false;
|
|
175
226
|
const typedCodes = transportCodes(transportFailure);
|
|
176
227
|
if (typedCodes.some(code => OVERFLOW_PROVIDER_CODES.has(code))) return true;
|
|
228
|
+
// A specific non-overflow cause (auth, quota, rate limit) is authoritative
|
|
229
|
+
// and can never be upgraded by error prose.
|
|
177
230
|
if (hasTypedNonOverflowCode(transportFailure)) return false;
|
|
231
|
+
// A generic envelope (`invalid_request_error`) names no cause. It still
|
|
232
|
+
// vetoes free-form overflow prose, but must not veto a provider-measured,
|
|
233
|
+
// self-verifying overflow report — that is how Anthropic reports overflow.
|
|
234
|
+
if (
|
|
235
|
+
typedCodes.some(code => GENERIC_ENVELOPE_PROVIDER_CODES.has(code)) &&
|
|
236
|
+
!hasSelfVerifyingOverflowMeasurement(message)
|
|
237
|
+
) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
178
240
|
if (isTypedNoBodyOverflow(message, transportFailure)) return true;
|
|
179
241
|
|
|
180
242
|
const errorMessage = message.errorMessage;
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import {
|
|
21
21
|
attachUnicodeEscapeEvidence,
|
|
22
|
-
|
|
22
|
+
collectUnsafeUnicodeEscapeEvidence,
|
|
23
23
|
parseJsonWithRepair,
|
|
24
24
|
type UnicodeEscapeEvidence,
|
|
25
25
|
} from "./json-parse";
|
|
@@ -243,9 +243,11 @@ export class ToolCallHealer {
|
|
|
243
243
|
const name = normalizeFunctionName(rawId);
|
|
244
244
|
const id = generateHealedToolCallId();
|
|
245
245
|
|
|
246
|
-
// Sample
|
|
247
|
-
// literal characters, so
|
|
248
|
-
|
|
246
|
+
// Sample unsafe raw payloads first: the round-trip below decodes `\uXXXX`
|
|
247
|
+
// into literal characters, so malformed escape data must be retained before
|
|
248
|
+
// normalization. Valid escapes need no evidence because their decoded value
|
|
249
|
+
// is canonical.
|
|
250
|
+
const escapedUnicodeArgumentEvidence = collectUnsafeUnicodeEscapeEvidence(rawArgs);
|
|
249
251
|
const escapedNonAsciiArguments = escapedUnicodeArgumentEvidence !== undefined;
|
|
250
252
|
|
|
251
253
|
let argsJson = rawArgs;
|