@gajae-code/ai 0.15.4 → 0.15.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,13 +2,24 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.15.5] - 2026-08-29
6
+
7
+ ### Added
8
+
9
+ - The built-in `kiro` provider now accepts long-lived `KIRO_API_KEY` values (`ksk_…` from [app.kiro.dev](https://app.kiro.dev/settings/api-keys)) in addition to AWS Builder ID OAuth. API-key auth talks to the Kiro service root with `tokentype: API_KEY` and discovers the live model catalog via `ListAvailableModels`. Builder ID login (`gjc auth-broker login kiro` / `AWS_BEARER_TOKEN_KIRO`) is unchanged.
10
+
11
+ ### Fixed
12
+
13
+ - The generic OpenAI Responses transport now preserves the typed `server_is_overloaded` code from an HTTP 200 terminal envelope as transport facts (`openaiErrorCode` / `providerCode`), and that statusless code classifies as a `server` fallback trigger. Both terminal shapes that carry the structured failure are covered: `response.failed`, and `response.completed` whose response `status` is `failed` (including the nested `status_details.error` form). Previously the envelope became a plain `Error`, so the only structured evidence of a capacity rejection was lost and consumers had to read provider prose. The code is matched case-sensitively and exactly, so near misses (`server_is_overloaded_now`), case variants (`SERVER_IS_OVERLOADED`), padded variants, and a `cancelled` response carrying the same code all stay untyped and unclassified; the displayed error message is unchanged in every case. The shared Azure Responses path inherits the same parsing. (#5018)
14
+ - Kiro credential selection now rejects control-character injection and keeps non-`ksk_` values on the Builder ID bearer path. API-key failures redact the credential before reaching assistant error output, and the transport accepts provider header overrides without changing the documented endpoint contract.
15
+ - API-key model discovery also retains the contributor-supplied static catalog for offline print-mode resolution, while live `ListAvailableModels` results remain authoritative when available.
5
16
  ## [0.15.4] - 2026-08-29
6
17
 
7
18
  ## [0.15.3] - 2026-08-27
8
19
 
9
20
  ### Fixed
10
21
 
11
- - 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.
22
+ - Valid JSON `\uXXXX` tool arguments now execute as their canonical decoded strings instead of entering the escaped-non-ASCII resample loop. This intentionally treats every syntactically valid decoded scalar as canonical; runtime validation cannot infer whether a valid hex digit differed from the model's intent. Provider adapters retain guard metadata for malformed escape-bearing JSON, duplicate/deep suspicious escape evidence, and unpaired UTF-16 surrogates, while standard escaped Hangul, emoji surrogate pairs, and printable ASCII no longer consume retries or terminate managed runs.
12
23
  - Explicit positive `maxTokens` values declared for custom `models.yml` models
13
24
  and model overrides now reach the provider request across the shared stream
14
25
  mapping (`max_tokens`, `max_completion_tokens`, and `max_output_tokens`).
@@ -682,8 +682,17 @@ export declare class AuthStorage {
682
682
  *
683
683
  * Lower priority than {@link setRuntimeApiKey} so a CLI `--api-key`
684
684
  * still wins for the duration of a single invocation.
685
+ *
686
+ * `options.envSourced` marks the value as resolved from a models.yml
687
+ * `apiKeyEnv` indirection. Unlike a literal pin, an env pointer only says
688
+ * where to look for a key; when the user has since run `auth login`, the
689
+ * stored api_key credential is the fresher, actively-managed secret and
690
+ * wins over the indirection (stored OAuth credentials still yield, so a
691
+ * custom-endpoint bearer is never replaced by an upstream OAuth token).
685
692
  */
686
- setConfigApiKey(provider: string, apiKey: string): void;
693
+ setConfigApiKey(provider: string, apiKey: string, options?: {
694
+ envSourced?: boolean;
695
+ }): void;
687
696
  /**
688
697
  * Remove a single config-sourced API key override.
689
698
  */
@@ -840,13 +849,16 @@ export declare class AuthStorage {
840
849
  * Get API key for a provider.
841
850
  * Priority:
842
851
  * 1. Runtime override (CLI --api-key)
843
- * 2. Config override (models.yml `providers.<name>.apiKey`)
844
- * 3. Session-selected OAuth credential, when present
845
- * 4. Usable or unresolved API key from storage
846
- * 5. OAuth token from storage (auto-refreshed)
847
- * 6. Previously unusable command-backed API key retry
848
- * 7. Environment variable
849
- * 8. Fallback resolver (models.yml custom providers, last-resort)
852
+ * 2. Config override (models.yml `providers.<name>.apiKey` literal pin)
853
+ * 3. Stored api_key credential from `auth login`, when the config override
854
+ * is only an `apiKeyEnv` indirection
855
+ * 4. Config override sourced from models.yml `providers.<name>.apiKeyEnv`
856
+ * 5. Session-selected OAuth credential, when present
857
+ * 6. Usable or unresolved API key from storage
858
+ * 7. OAuth token from storage (auto-refreshed)
859
+ * 8. Previously unusable command-backed API key retry
860
+ * 9. Environment variable
861
+ * 10. Fallback resolver (models.yml custom providers, last-resort)
850
862
  */
851
863
  getApiKey(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<string | undefined>;
852
864
  /**
@@ -915,7 +927,9 @@ export declare class AuthStorage {
915
927
  *
916
928
  * Surfaces four layers, highest precedence first:
917
929
  * 1. Runtime override (`--api-key`).
918
- * 2. Config override (`models.yml` `providers.<name>.apiKey`).
930
+ * 2. Config override (`models.yml` `providers.<name>.apiKey` literal pin,
931
+ * or an `apiKeyEnv` indirection when no stored api_key credential
932
+ * outranks it).
919
933
  * 3. Stored credential (the one this session is currently sticky to, or the
920
934
  * one round-robin would pick next when no session id is supplied).
921
935
  * 4. Env var / fallback resolver — when no stored credential exists.
@@ -24,5 +24,6 @@ export interface JetBrainsJunieModelManagerConfig {
24
24
  }
25
25
  export declare function jetbrainsJunieModelManagerOptions(_config?: JetBrainsJunieModelManagerConfig): ModelManagerOptions<"anthropic-messages">;
26
26
  export interface KiroModelManagerConfig {
27
+ apiKey?: string;
27
28
  }
28
- export declare function kiroModelManagerOptions(_config?: KiroModelManagerConfig): ModelManagerOptions<"kiro-codewhisperer-stream">;
29
+ export declare function kiroModelManagerOptions(config?: KiroModelManagerConfig): ModelManagerOptions<"kiro-codewhisperer-stream">;
@@ -0,0 +1,50 @@
1
+ import type { Model, StreamFunction } from "../types";
2
+ export declare function isKiroApiKey(value: string | undefined): value is string;
3
+ export declare function kiroApiRegion(options?: {
4
+ region?: string;
5
+ }): string;
6
+ export declare function kiroApiBaseUrl(region: string): string;
7
+ export declare function toKiroModelId(modelId: string): string;
8
+ export declare function kiroApiStaticModels(): Model<"kiro-codewhisperer-stream">[];
9
+ /** Discover models this API key can use. Returns null when the key is missing. */
10
+ export declare function fetchKiroApiModels(apiKey: string, region?: string): Promise<Model<"kiro-codewhisperer-stream">[]>;
11
+ type KiroStreamEvent = {
12
+ type: "content";
13
+ data: string;
14
+ } | {
15
+ type: "toolUse";
16
+ data: {
17
+ name: string;
18
+ toolUseId: string;
19
+ input: string;
20
+ stop?: boolean;
21
+ };
22
+ } | {
23
+ type: "toolUseInput";
24
+ data: {
25
+ input: string;
26
+ };
27
+ } | {
28
+ type: "toolUseStop";
29
+ data: {
30
+ stop: boolean;
31
+ };
32
+ } | {
33
+ type: "usage";
34
+ data: {
35
+ inputTokens?: number;
36
+ outputTokens?: number;
37
+ };
38
+ } | {
39
+ type: "error";
40
+ data: {
41
+ error: string;
42
+ message?: string;
43
+ };
44
+ };
45
+ export declare function parseKiroApiEvents(buffer: string): {
46
+ events: KiroStreamEvent[];
47
+ remaining: string;
48
+ };
49
+ export declare const streamKiroApiKey: StreamFunction<"kiro-codewhisperer-stream">;
50
+ export {};
@@ -1,5 +1,8 @@
1
+ import type { Effort } from "../model-thinking";
1
2
  import type { StreamFunction, StreamOptions } from "../types";
2
3
  export interface KiroCodeWhispererOptions extends StreamOptions {
4
+ /** Effort level for Kiro API-key reasoning. */
5
+ reasoning?: Effort | boolean;
3
6
  /** AWS region for the CodeWhisperer streaming endpoint. */
4
7
  region?: string;
5
8
  /** Profile ARN for enterprise IAM Identity Center accounts. */
@@ -399,31 +399,22 @@ export interface ToolCall {
399
399
  */
400
400
  incompleteArgumentsReason?: "truncated" | "malformed" | "conflicting" | "ambiguous";
401
401
  /**
402
- * Set when the raw argument JSON spelled a printable character as a `\uXXXX`
403
- * escape instead of a literal character. This includes ASCII landings because
404
- * a one-nibble mutation can move an intended non-ASCII scalar below U+0080.
405
- * Such a payload parses cleanly but
406
- * is unverifiable: one mistyped hex digit decodes to a different, equally
407
- * valid character, so the text can be silently wrong with no in-band evidence.
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
414
- * content, ids, or durable metadata.
415
- * Escapes that are required (control characters) or unavoidable (lone
416
- * surrogates) never set this.
402
+ * Set by current producers when raw argument JSON carries unsafe Unicode
403
+ * data, such as malformed escape evidence or a decoded unpaired surrogate.
404
+ * Valid JSON `\uXXXX` escapes are canonical spellings of the decoded string
405
+ * and current producers do not set this flag for them.
406
+ *
407
+ * Legacy producers may still set the flag for any escaped non-ASCII spelling.
408
+ * The agent loop keeps its bounded legacy resample/display-safe behavior for
409
+ * those calls while consuming the transient evidence below.
417
410
  */
418
411
  escapedNonAsciiArguments?: boolean;
419
412
  /**
420
- * Bounded, payload-free evidence for the original raw escape positions and
421
- * process-keyed scalar/path identities. Required for the display-safe terminal exemption: decoded values
422
- * alone cannot prove that an ASCII landing such as `\u0077` was not a
423
- * one-nibble mutation of a non-ASCII escape. Presence of this evidence implies
424
- * the guarded state even if a legacy producer omitted
425
- * `escapedNonAsciiArguments`. The agent consumes and removes this transient
426
- * field before the tool-call message can become durable.
413
+ * Bounded, payload-free evidence for raw Unicode argument data. Current
414
+ * producers attach it only for unsafe data; legacy producers may attach
415
+ * non-malformed positional evidence used by the display-safe compatibility
416
+ * path. The agent consumes and removes this transient field before the
417
+ * tool-call message can become durable.
427
418
  */
428
419
  escapedUnicodeArgumentEvidence?: UnicodeEscapeEvidence;
429
420
  }
@@ -23,6 +23,13 @@ export interface FallbackTrigger {
23
23
  export declare const STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE = "stream_first_event_timeout";
24
24
  /** Stable code for a nominally successful response with no content or token usage. */
25
25
  export declare const EMPTY_RESPONSE_PROVIDER_CODE = "empty_response";
26
+ /**
27
+ * OpenAI's typed capacity-overload code. It arrives without an HTTP status —
28
+ * inside an HTTP 200 terminal Responses envelope or a Codex error event — so the
29
+ * code itself is the only structured evidence of the failure and must survive
30
+ * the existence gate below. It is always compared case-sensitively.
31
+ */
32
+ export declare const SERVER_OVERLOADED_PROVIDER_CODE = "server_is_overloaded";
26
33
  export type TransportHeaders = Headers | Record<string, string | undefined>;
27
34
  /**
28
35
  * Structured facts from an upstream HTTP or transport failure. Retry decisions
@@ -67,9 +67,11 @@ export declare function collectUnicodeEscapeEvidence(json: string): UnicodeEscap
67
67
  /**
68
68
  * Return evidence only when decoded tool arguments are unsafe to execute.
69
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.
70
+ * Valid JSON escapes and literal UTF-8 have the same canonical decoded value,
71
+ * including a valid scalar whose hex digits differ from what a caller intended:
72
+ * runtime syntax validation cannot infer author intent after decoding.
73
+ * Malformed escape-bearing JSON, duplicate/deep suspicious escape evidence, and
74
+ * unpaired UTF-16 surrogates keep the fail-closed path.
73
75
  */
74
76
  export declare function collectUnsafeUnicodeEscapeEvidence(json: string): UnicodeEscapeEvidence | undefined;
75
77
  /** Attach unsafe raw evidence while preserving the existing call-level guard flag. */
@@ -22,10 +22,10 @@ export interface HealedToolCall {
22
22
  readonly name: string;
23
23
  readonly arguments: string;
24
24
  /**
25
- * Whether the raw leaked payload spelled a printable non-ASCII character as a
26
- * `\uXXXX` escape. Captured BEFORE the normalizing round-trip below, which
27
- * decodes escapes into literal characters and would otherwise erase the only
28
- * evidence that the text is unverifiable.
25
+ * Whether the raw leaked payload carried unsafe Unicode argument data.
26
+ * Captured before the normalizing round-trip below, which would otherwise
27
+ * erase malformed escape evidence. Valid `\uXXXX` spellings decode
28
+ * canonically and leave this false.
29
29
  */
30
30
  readonly escapedNonAsciiArguments: boolean;
31
31
  readonly escapedUnicodeArgumentEvidence?: UnicodeEscapeEvidence;
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",
4
+ "version": "0.15.5",
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.4",
44
- "@gajae-code/utils": "0.15.4",
43
+ "@gajae-code/natives": "0.15.5",
44
+ "@gajae-code/utils": "0.15.5",
45
45
  "openai": "^6.36.0",
46
46
  "partial-json": "^0.1.7",
47
47
  "zod": "4.4.3"
@@ -1231,6 +1231,14 @@ export class AuthStorage {
1231
1231
  #data: Map<string, StoredCredential[]> = new Map();
1232
1232
  #runtimeOverrides: Map<string, string> = new Map();
1233
1233
  #configOverrides: Map<string, string> = new Map();
1234
+ /**
1235
+ * Providers whose config override was resolved from a models.yml `apiKeyEnv`
1236
+ * indirection rather than a literal `apiKey` pin. An env pointer is not a
1237
+ * pinned secret: the pointed-to value (shell env, trusted env files) can go
1238
+ * stale silently, and the 401 rotation machinery cannot repair it. A stored
1239
+ * api_key credential from `auth login` therefore outranks it.
1240
+ */
1241
+ #configOverrideEnvSourced: Set<string> = new Set();
1234
1242
  #runtimeCredentialSelectors: Map<string, AuthCredentialSelector> = new Map();
1235
1243
  /** Soft runtime credential preference per provider; quota failures may rotate away from it. */
1236
1244
  #runtimePreferredCredentialSelectors: Map<string, AuthCredentialSelector> = new Map();
@@ -1926,10 +1934,22 @@ export class AuthStorage {
1926
1934
  *
1927
1935
  * Lower priority than {@link setRuntimeApiKey} so a CLI `--api-key`
1928
1936
  * still wins for the duration of a single invocation.
1937
+ *
1938
+ * `options.envSourced` marks the value as resolved from a models.yml
1939
+ * `apiKeyEnv` indirection. Unlike a literal pin, an env pointer only says
1940
+ * where to look for a key; when the user has since run `auth login`, the
1941
+ * stored api_key credential is the fresher, actively-managed secret and
1942
+ * wins over the indirection (stored OAuth credentials still yield, so a
1943
+ * custom-endpoint bearer is never replaced by an upstream OAuth token).
1929
1944
  */
1930
- setConfigApiKey(provider: string, apiKey: string): void {
1945
+ setConfigApiKey(provider: string, apiKey: string, options: { envSourced?: boolean } = {}): void {
1931
1946
  const storageProvider = resolveOAuthStorageProvider(provider);
1932
1947
  this.#configOverrides.set(storageProvider, apiKey);
1948
+ if (options.envSourced) {
1949
+ this.#configOverrideEnvSourced.add(storageProvider);
1950
+ } else {
1951
+ this.#configOverrideEnvSourced.delete(storageProvider);
1952
+ }
1933
1953
  this.#bumpGeneration("set-config-api-key", storageProvider);
1934
1954
  }
1935
1955
 
@@ -1938,6 +1958,7 @@ export class AuthStorage {
1938
1958
  */
1939
1959
  removeConfigApiKey(provider: string): void {
1940
1960
  const storageProvider = resolveOAuthStorageProvider(provider);
1961
+ this.#configOverrideEnvSourced.delete(storageProvider);
1941
1962
  if (this.#configOverrides.delete(storageProvider)) this.#bumpGeneration("remove-config-api-key", storageProvider);
1942
1963
  }
1943
1964
 
@@ -1947,6 +1968,7 @@ export class AuthStorage {
1947
1968
  */
1948
1969
  clearConfigApiKeys(): void {
1949
1970
  const providers = [...this.#configOverrides.keys()];
1971
+ this.#configOverrideEnvSourced.clear();
1950
1972
  if (providers.length === 0) return;
1951
1973
  this.#configOverrides.clear();
1952
1974
  for (const provider of providers) this.#bumpGeneration("clear-config-api-keys", provider);
@@ -5165,9 +5187,16 @@ export class AuthStorage {
5165
5187
  if (runtimeKey) return runtimeKey;
5166
5188
 
5167
5189
  const configKey = this.#configOverrides.get(provider);
5168
- if (configKey) return configKey;
5190
+ if (configKey && !this.#configOverrideEnvSourced.has(provider)) return configKey;
5169
5191
 
5170
5192
  const selectedCredential = this.#resolveSelectedStoredCredential(provider, undefined, undefined);
5193
+ if (configKey) {
5194
+ // Env-sourced (`apiKeyEnv`) override: same precedence as getApiKey —
5195
+ // a stored api_key credential from `auth login` wins, stored OAuth
5196
+ // still yields to the indirection.
5197
+ const storedApiKey = await this.#resolveStoredApiKeyOverEnvConfig(provider, selectedCredential, undefined);
5198
+ return storedApiKey ?? configKey;
5199
+ }
5171
5200
  if (selectedCredential?.credential.type === "api_key") {
5172
5201
  return this.#resolveStoredApiKey(provider, selectedCredential.credential.key);
5173
5202
  }
@@ -5217,13 +5246,16 @@ export class AuthStorage {
5217
5246
  * Get API key for a provider.
5218
5247
  * Priority:
5219
5248
  * 1. Runtime override (CLI --api-key)
5220
- * 2. Config override (models.yml `providers.<name>.apiKey`)
5221
- * 3. Session-selected OAuth credential, when present
5222
- * 4. Usable or unresolved API key from storage
5223
- * 5. OAuth token from storage (auto-refreshed)
5224
- * 6. Previously unusable command-backed API key retry
5225
- * 7. Environment variable
5226
- * 8. Fallback resolver (models.yml custom providers, last-resort)
5249
+ * 2. Config override (models.yml `providers.<name>.apiKey` literal pin)
5250
+ * 3. Stored api_key credential from `auth login`, when the config override
5251
+ * is only an `apiKeyEnv` indirection
5252
+ * 4. Config override sourced from models.yml `providers.<name>.apiKeyEnv`
5253
+ * 5. Session-selected OAuth credential, when present
5254
+ * 6. Usable or unresolved API key from storage
5255
+ * 7. OAuth token from storage (auto-refreshed)
5256
+ * 8. Previously unusable command-backed API key retry
5257
+ * 9. Environment variable
5258
+ * 10. Fallback resolver (models.yml custom providers, last-resort)
5227
5259
  */
5228
5260
  async getApiKey(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<string | undefined> {
5229
5261
  provider = resolveOAuthStorageProvider(provider);
@@ -5239,7 +5271,17 @@ export class AuthStorage {
5239
5271
  // honor it instead of forwarding an upstream OAuth token that the proxy
5240
5272
  // won't accept.
5241
5273
  const configKey = this.#configOverrides.get(provider);
5242
- if (configKey) return configKey;
5274
+ if (configKey) {
5275
+ if (!this.#configOverrideEnvSourced.has(provider)) return configKey;
5276
+ // The override is an `apiKeyEnv` indirection, not a pinned value. A
5277
+ // stored api_key credential from `auth login` is actively managed
5278
+ // (validated at login, rotated on 401), while the pointed-to env
5279
+ // value can go stale with no recovery path — prefer the stored
5280
+ // credential. Stored OAuth credentials still yield to the override.
5281
+ const storedApiKey = await this.#resolveStoredApiKeyOverEnvConfig(provider, selectedCredential, sessionId);
5282
+ if (storedApiKey) return storedApiKey;
5283
+ return configKey;
5284
+ }
5243
5285
 
5244
5286
  if (selectedCredential?.credential.type === "api_key") {
5245
5287
  this.#recordSessionCredential(provider, sessionId, "api_key", selectedCredential.index);
@@ -5295,6 +5337,38 @@ export class AuthStorage {
5295
5337
  return this.#fallbackResolver?.(provider) ?? undefined;
5296
5338
  }
5297
5339
 
5340
+ /**
5341
+ * Resolve a stored api_key credential that outranks an env-sourced config
5342
+ * override (`apiKeyEnv`). Mirrors the api_key branches of {@link getApiKey}:
5343
+ * the selector-pinned credential first, then the round-robin/session pool.
5344
+ * Returns undefined when no stored api_key credential resolves, leaving the
5345
+ * env-sourced override in effect.
5346
+ */
5347
+ async #resolveStoredApiKeyOverEnvConfig(
5348
+ provider: string,
5349
+ selectedCredential: ({ index: number } & StoredCredential) | undefined,
5350
+ sessionId?: string,
5351
+ ): Promise<string | undefined> {
5352
+ if (selectedCredential?.credential.type === "api_key") {
5353
+ const resolved = await this.#resolveStoredApiKey(provider, selectedCredential.credential.key);
5354
+ if (resolved) {
5355
+ this.#recordSessionCredential(provider, sessionId, "api_key", selectedCredential.index);
5356
+ return resolved;
5357
+ }
5358
+ }
5359
+ const attemptedApiKeyIndices = new Set<number>();
5360
+ for (;;) {
5361
+ const apiKeySelection = this.#selectApiKeyCredential(provider, sessionId, attemptedApiKeyIndices);
5362
+ if (!apiKeySelection) return undefined;
5363
+ attemptedApiKeyIndices.add(apiKeySelection.index);
5364
+ const resolved = await this.#resolveStoredApiKey(provider, apiKeySelection.credential.key);
5365
+ if (resolved) {
5366
+ this.#recordSessionCredential(provider, sessionId, "api_key", apiKeySelection.index);
5367
+ return resolved;
5368
+ }
5369
+ }
5370
+ }
5371
+
5298
5372
  /**
5299
5373
  * Resolve the OAuth credential for `provider`, refreshing through the same
5300
5374
  * pipeline as {@link AuthStorage.getApiKey} but returning the refreshed
@@ -5621,7 +5695,9 @@ export class AuthStorage {
5621
5695
  *
5622
5696
  * Surfaces four layers, highest precedence first:
5623
5697
  * 1. Runtime override (`--api-key`).
5624
- * 2. Config override (`models.yml` `providers.<name>.apiKey`).
5698
+ * 2. Config override (`models.yml` `providers.<name>.apiKey` literal pin,
5699
+ * or an `apiKeyEnv` indirection when no stored api_key credential
5700
+ * outranks it).
5625
5701
  * 3. Stored credential (the one this session is currently sticky to, or the
5626
5702
  * one round-robin would pick next when no session id is supplied).
5627
5703
  * 4. Env var / fallback resolver — when no stored credential exists.
@@ -5633,7 +5709,12 @@ export class AuthStorage {
5633
5709
  return "runtime override (--api-key)";
5634
5710
  }
5635
5711
  if (this.#configOverrides.has(provider)) {
5636
- return "config override (models.yml)";
5712
+ // An `apiKeyEnv` indirection loses to a stored api_key credential
5713
+ // (see getApiKey); describe the credential that actually wins.
5714
+ const shadowed = this.#getStoredCredentials(provider).some(entry => entry.credential.type === "api_key");
5715
+ if (!this.#configOverrideEnvSourced.has(provider) || !shadowed) {
5716
+ return "config override (models.yml)";
5717
+ }
5637
5718
  }
5638
5719
 
5639
5720
  const baseLabel = this.#sourceLabel ?? "local store";
@@ -549,6 +549,22 @@ function applyGeneratedModelPolicy(model: ApiModel<Api>): void {
549
549
  levels: [Effort.Low, Effort.High, Effort.Max],
550
550
  };
551
551
  }
552
+ // GLM-5.3-Flash keeps GLM-5.3's text contract: 1M context, 128K output, and
553
+ // the same always-on low/high/max reasoning_effort. models.dev currently
554
+ // lists it with null limits, so pin the contract the same way.
555
+ // https://docs.z.ai/guides/llm/glm-5.3-flash ("Text parameters are
556
+ // consistent with GLM-5.3, with support for a 1M-token context window.")
557
+ if (model.provider === "zai" && model.id === "glm-5.3-flash") {
558
+ model.contextWindow = 1_000_000;
559
+ model.maxTokens = 131_072;
560
+ model.thinking = {
561
+ mode: "effort",
562
+ minLevel: Effort.Low,
563
+ maxLevel: Effort.Max,
564
+ defaultLevel: Effort.Max,
565
+ levels: [Effort.Low, Effort.High, Effort.Max],
566
+ };
567
+ }
552
568
  // Groq's agentic `compound` systems reject `reasoning_effort` outright
553
569
  // (400 "`reasoning_effort` is not supported with this model", verified
554
570
  // 2026-08-23), yet models.dev advertises them as reasoning models. Drop the
package/src/models.json CHANGED
@@ -89307,6 +89307,37 @@
89307
89307
  ]
89308
89308
  }
89309
89309
  },
89310
+ "glm-5.3-flash": {
89311
+ "id": "glm-5.3-flash",
89312
+ "name": "GLM-5.3-Flash",
89313
+ "api": "anthropic-messages",
89314
+ "provider": "zai",
89315
+ "baseUrl": "https://api.z.ai/api/anthropic",
89316
+ "reasoning": true,
89317
+ "input": [
89318
+ "text",
89319
+ "image"
89320
+ ],
89321
+ "cost": {
89322
+ "input": 0,
89323
+ "output": 0,
89324
+ "cacheRead": 0,
89325
+ "cacheWrite": 0
89326
+ },
89327
+ "contextWindow": 1000000,
89328
+ "maxTokens": 131072,
89329
+ "thinking": {
89330
+ "mode": "effort",
89331
+ "minLevel": "low",
89332
+ "maxLevel": "max",
89333
+ "defaultLevel": "max",
89334
+ "levels": [
89335
+ "low",
89336
+ "high",
89337
+ "max"
89338
+ ]
89339
+ }
89340
+ },
89310
89341
  "glm-5v-turbo": {
89311
89342
  "id": "glm-5v-turbo",
89312
89343
  "name": "GLM-5V-Turbo",
package/src/models.ts CHANGED
@@ -29,7 +29,15 @@ const providerModelRegistry: Map<string, Map<string, Model<Api>>> = new Map();
29
29
  function getBundledCatalog(): BundledCatalog {
30
30
  // TS types a .json import as its contents; at runtime `with { type: "file" }`
31
31
  // yields the file path (bunfs path in compiled binaries, disk path in dev).
32
- bundledCatalog ??= JSON.parse(readFileSync(modelsJsonPath as unknown as string, "utf8")) as BundledCatalog;
32
+ if (bundledCatalog !== undefined) return bundledCatalog;
33
+ const resource = modelsJsonPath as unknown;
34
+ if (typeof resource === "string") {
35
+ bundledCatalog = JSON.parse(readFileSync(resource, "utf8")) as BundledCatalog;
36
+ } else if (resource !== null && typeof resource === "object") {
37
+ bundledCatalog = resource as BundledCatalog;
38
+ } else {
39
+ throw new Error("Bundled models catalog resource is invalid.");
40
+ }
33
41
  return bundledCatalog;
34
42
  }
35
43
 
@@ -3,6 +3,7 @@ import { once, sanitizeText } from "@gajae-code/utils";
3
3
 
4
4
  import type { ModelManagerOptions } from "../model-manager";
5
5
  import { buildZCodeSourceHeaders, resolveGlmZcodeAnthropicBaseUrl } from "../providers/anthropic";
6
+ import { fetchKiroApiModels, isKiroApiKey, kiroApiStaticModels } from "../providers/kiro-api-key";
6
7
  import { fetchOpenCodexModels, OPENCODEX_MODEL_CACHE_TTL_MS } from "../providers/openai-opencodex-responses";
7
8
  import { fetchCodexModels } from "../utils/discovery/codex";
8
9
  import { fetchOpenAICompatibleModels } from "../utils/discovery/openai-compatible";
@@ -164,10 +165,21 @@ export function jetbrainsJunieModelManagerOptions(
164
165
  // Kiro (Amazon Q Developer / CodeWhisperer)
165
166
  // ---------------------------------------------------------------------------
166
167
 
167
- export interface KiroModelManagerConfig {}
168
+ export interface KiroModelManagerConfig {
169
+ apiKey?: string;
170
+ }
168
171
 
169
172
  export function kiroModelManagerOptions(
170
- _config: KiroModelManagerConfig = {},
173
+ config: KiroModelManagerConfig = {},
171
174
  ): ModelManagerOptions<"kiro-codewhisperer-stream"> {
172
- return { providerId: "kiro" };
175
+ const apiKey = config.apiKey;
176
+ return {
177
+ providerId: "kiro",
178
+ ...(isKiroApiKey(apiKey)
179
+ ? {
180
+ staticModels: kiroApiStaticModels(),
181
+ fetchDynamicModels: () => fetchKiroApiModels(apiKey),
182
+ }
183
+ : undefined),
184
+ };
173
185
  }
@@ -0,0 +1,50 @@
1
+ import type { Model, StreamFunction } from "../types";
2
+ export declare function isKiroApiKey(value: string | undefined): value is string;
3
+ export declare function kiroApiRegion(options?: {
4
+ region?: string;
5
+ }): string;
6
+ export declare function kiroApiBaseUrl(region: string): string;
7
+ export declare function toKiroModelId(modelId: string): string;
8
+ export declare function kiroApiStaticModels(): Model<"kiro-codewhisperer-stream">[];
9
+ /** Discover models this API key can use. Returns null when the key is missing. */
10
+ export declare function fetchKiroApiModels(apiKey: string, region?: string): Promise<Model<"kiro-codewhisperer-stream">[]>;
11
+ type KiroStreamEvent = {
12
+ type: "content";
13
+ data: string;
14
+ } | {
15
+ type: "toolUse";
16
+ data: {
17
+ name: string;
18
+ toolUseId: string;
19
+ input: string;
20
+ stop?: boolean;
21
+ };
22
+ } | {
23
+ type: "toolUseInput";
24
+ data: {
25
+ input: string;
26
+ };
27
+ } | {
28
+ type: "toolUseStop";
29
+ data: {
30
+ stop: boolean;
31
+ };
32
+ } | {
33
+ type: "usage";
34
+ data: {
35
+ inputTokens?: number;
36
+ outputTokens?: number;
37
+ };
38
+ } | {
39
+ type: "error";
40
+ data: {
41
+ error: string;
42
+ message?: string;
43
+ };
44
+ };
45
+ export declare function parseKiroApiEvents(buffer: string): {
46
+ events: KiroStreamEvent[];
47
+ remaining: string;
48
+ };
49
+ export declare const streamKiroApiKey: StreamFunction<"kiro-codewhisperer-stream">;
50
+ export {};