@gajae-code/ai 0.18.11 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +1 -1
  3. package/dist/types/auth-broker/wire-schemas.d.ts +30 -0
  4. package/dist/types/auth-storage.d.ts +98 -13
  5. package/dist/types/bedrock-claude-cache-policy.d.ts +1 -1
  6. package/dist/types/model-cache.d.ts +6 -4
  7. package/dist/types/model-manager.d.ts +2 -0
  8. package/dist/types/model-pricing.d.ts +8 -0
  9. package/dist/types/model-thinking.d.ts +11 -0
  10. package/dist/types/openai-completions-compat.d.ts +2 -1
  11. package/dist/types/provider-models/google.d.ts +2 -0
  12. package/dist/types/providers/amazon-bedrock.d.ts +17 -2
  13. package/dist/types/providers/anthropic.d.ts +25 -0
  14. package/dist/types/providers/aws-credential-config.d.ts +5 -0
  15. package/dist/types/providers/google-auth.d.ts +7 -0
  16. package/dist/types/providers/google-gemini-headers.d.ts +11 -6
  17. package/dist/types/providers/kiro-api-key.d.ts +5 -0
  18. package/dist/types/providers/openai-chat-server-schema.d.ts +2 -1
  19. package/dist/types/providers/openai-completions.d.ts +1 -1
  20. package/dist/types/providers/openai-responses-shared.d.ts +2 -1
  21. package/dist/types/stream.d.ts +33 -1
  22. package/dist/types/types.d.ts +26 -3
  23. package/dist/types/usage.d.ts +3 -0
  24. package/dist/types/utils/discovery/antigravity.d.ts +16 -1
  25. package/dist/types/utils/idle-iterator.d.ts +3 -2
  26. package/dist/types/utils/oauth/glm-zcode.d.ts +0 -30
  27. package/dist/types/utils/oauth/types.d.ts +9 -0
  28. package/package.json +4 -4
  29. package/src/auth-broker/remote-store.ts +62 -11
  30. package/src/auth-broker/wire-schemas.ts +2 -0
  31. package/src/auth-gateway/server.ts +3 -2
  32. package/src/auth-storage.ts +908 -246
  33. package/src/bedrock-claude-cache-policy.d.ts +1 -1
  34. package/src/bedrock-claude-cache-policy.ts +2 -2
  35. package/src/model-cache.ts +45 -10
  36. package/src/model-manager.ts +18 -6
  37. package/src/model-pricing.d.ts +8 -0
  38. package/src/model-pricing.ts +87 -0
  39. package/src/model-thinking.d.ts +11 -0
  40. package/src/model-thinking.ts +97 -12
  41. package/src/models.json +3176 -1670
  42. package/src/models.ts +10 -3
  43. package/src/openai-completions-compat.d.ts +2 -1
  44. package/src/openai-completions-compat.ts +5 -0
  45. package/src/provider-models/google.ts +11 -1
  46. package/src/provider-models/openai-compat.ts +26 -0
  47. package/src/providers/amazon-bedrock.d.ts +17 -2
  48. package/src/providers/amazon-bedrock.ts +76 -16
  49. package/src/providers/anthropic.d.ts +25 -0
  50. package/src/providers/anthropic.ts +230 -18
  51. package/src/providers/aws-credential-config.d.ts +5 -0
  52. package/src/providers/aws-credential-config.ts +29 -1
  53. package/src/providers/google-auth.d.ts +7 -0
  54. package/src/providers/google-auth.ts +165 -4
  55. package/src/providers/google-gemini-headers.d.ts +11 -6
  56. package/src/providers/google-gemini-headers.ts +16 -11
  57. package/src/providers/google-vertex.ts +2 -4
  58. package/src/providers/kiro-api-key.d.ts +5 -0
  59. package/src/providers/kiro-api-key.ts +379 -68
  60. package/src/providers/ollama.ts +19 -9
  61. package/src/providers/openai-anthropic-shim.ts +1 -0
  62. package/src/providers/openai-chat-server-schema.ts +1 -1
  63. package/src/providers/openai-chat-server.ts +14 -2
  64. package/src/providers/openai-codex-responses.ts +21 -14
  65. package/src/providers/openai-completions.d.ts +1 -1
  66. package/src/providers/openai-completions.ts +65 -4
  67. package/src/providers/openai-responses-server.ts +10 -1
  68. package/src/providers/openai-responses-shared.d.ts +2 -1
  69. package/src/providers/openai-responses-shared.ts +42 -1
  70. package/src/providers/openai-responses.ts +51 -10
  71. package/src/providers/pi-native-server.ts +1 -1
  72. package/src/providers/transform-messages.ts +32 -1
  73. package/src/stream.d.ts +33 -1
  74. package/src/stream.ts +111 -17
  75. package/src/types.d.ts +26 -3
  76. package/src/types.ts +55 -2
  77. package/src/usage/claude.ts +4 -2
  78. package/src/usage.ts +3 -0
  79. package/src/utils/discovery/antigravity.ts +192 -7
  80. package/src/utils/idle-iterator.d.ts +3 -2
  81. package/src/utils/idle-iterator.ts +30 -10
  82. package/src/utils/oauth/anthropic.ts +23 -22
  83. package/src/utils/oauth/api-key-validation.ts +34 -11
  84. package/src/utils/oauth/glm-zcode.d.ts +0 -30
  85. package/src/utils/oauth/glm-zcode.ts +15 -4
  86. package/src/utils/oauth/openai-codex.ts +41 -6
  87. package/src/utils/oauth/types.d.ts +9 -0
  88. package/src/utils/oauth/types.ts +9 -0
  89. package/src/utils/parse-bind.d.ts +35 -0
  90. package/src/utils/proxy.ts +35 -6
@@ -10,7 +10,7 @@ export type BedrockClaudeGeneration = {
10
10
  * - family-first (3.x era): anthropic.claude-3-5-haiku-20241022-v1:0
11
11
  * - kind-first (4+ era): anthropic.claude-opus-4-20250514-v1:0,
12
12
  * anthropic.claude-haiku-4-5-20251001-v1:0
13
- * Cross-region profiles (us./eu./au./jp./apac./global. prefixes) and inference-profile
13
+ * Cross-region profiles (us./eu./au./jp./apac./global./in. prefixes) and inference-profile
14
14
  * ARNs keep the canonical model id as their final path segment.
15
15
  */
16
16
  export declare function parseBedrockClaudeGeneration(id: string): BedrockClaudeGeneration | undefined;
@@ -20,13 +20,13 @@ function extractBedrockModelId(id: string): string | undefined {
20
20
  * - family-first (3.x era): anthropic.claude-3-5-haiku-20241022-v1:0
21
21
  * - kind-first (4+ era): anthropic.claude-opus-4-20250514-v1:0,
22
22
  * anthropic.claude-haiku-4-5-20251001-v1:0
23
- * Cross-region profiles (us./eu./au./jp./apac./global. prefixes) and inference-profile
23
+ * Cross-region profiles (us./eu./au./jp./apac./global./in. prefixes) and inference-profile
24
24
  * ARNs keep the canonical model id as their final path segment.
25
25
  */
26
26
  export function parseBedrockClaudeGeneration(id: string): BedrockClaudeGeneration | undefined {
27
27
  const modelId = extractBedrockModelId(id);
28
28
  if (modelId === undefined) return undefined;
29
- const prefix = "(?:(?:us|eu|au|jp|apac|global)\\.)?anthropic\\.claude-";
29
+ const prefix = "(?:(?:us|eu|au|jp|apac|global|in)\\.)?anthropic\\.claude-";
30
30
  const component = "(?:0|[1-9]\\d?)";
31
31
  const suffix = "(?:-(?:[a-z][a-z0-9]*(?::[a-z0-9]+)?|\\d{8}))*";
32
32
  const familyFirst = new RegExp(`^${prefix}([1-9]\\d?)(?:-(${component}))?-([a-z][a-z0-9]*)${suffix}$`).exec(modelId);
@@ -6,7 +6,7 @@ import { Database } from "bun:sqlite";
6
6
  import { getModelDbPath } from "@gajae-code/utils/dirs";
7
7
  import type { Api, Model } from "./types";
8
8
 
9
- const CACHE_SCHEMA_VERSION = 5;
9
+ const CACHE_SCHEMA_VERSION = 6;
10
10
 
11
11
  interface CacheRow {
12
12
  provider_id: string;
@@ -16,6 +16,7 @@ interface CacheRow {
16
16
  static_fingerprint: string;
17
17
  dynamic_model_ids: string | null;
18
18
  dynamic_model_provenance: string | null;
19
+ discovery_user_agent: string | null;
19
20
  models: string;
20
21
  }
21
22
 
@@ -38,6 +39,8 @@ interface CacheEntry<TApi extends Api = Api> {
38
39
  /** IDs returned by the authoritative dynamic provider catalog, when retained. */
39
40
  dynamicModelIds: string[] | undefined;
40
41
  dynamicModelProvenance: string | undefined;
42
+ /** User-Agent used during discovery; invalidates cache if it changes. */
43
+ discoveryUserAgent: string | undefined;
41
44
  }
42
45
 
43
46
  let sharedDb: Database | null = null;
@@ -63,6 +66,7 @@ function getDb(dbPath?: string): Database {
63
66
  static_fingerprint TEXT NOT NULL DEFAULT '',
64
67
  dynamic_model_ids TEXT,
65
68
  dynamic_model_provenance TEXT,
69
+ discovery_user_agent TEXT,
66
70
  models TEXT NOT NULL
67
71
  )
68
72
  `);
@@ -84,7 +88,13 @@ export function closeModelCache(dbPath?: string): boolean {
84
88
  }
85
89
 
86
90
  function migrateCacheSchema(db: Database): void {
87
- const columns = db.prepare("PRAGMA table_info(model_cache)").all() as TableInfoRow[];
91
+ const tableInfo = db.query("PRAGMA table_info(model_cache)");
92
+ let columns: TableInfoRow[];
93
+ try {
94
+ columns = tableInfo.all() as TableInfoRow[];
95
+ } finally {
96
+ tableInfo.finalize();
97
+ }
88
98
  if (!columns.some(column => column.name === "static_fingerprint")) {
89
99
  db.run("ALTER TABLE model_cache ADD COLUMN static_fingerprint TEXT NOT NULL DEFAULT ''");
90
100
  }
@@ -94,7 +104,10 @@ function migrateCacheSchema(db: Database): void {
94
104
  if (!columns.some(column => column.name === "dynamic_model_provenance")) {
95
105
  db.run("ALTER TABLE model_cache ADD COLUMN dynamic_model_provenance TEXT");
96
106
  }
97
- db.run("UPDATE model_cache SET version = ? WHERE version IN (2, 3, 4)", [CACHE_SCHEMA_VERSION]);
107
+ if (!columns.some(column => column.name === "discovery_user_agent")) {
108
+ db.run("ALTER TABLE model_cache ADD COLUMN discovery_user_agent TEXT");
109
+ }
110
+ db.run("UPDATE model_cache SET version = ? WHERE version IN (2, 3, 4, 5)", [CACHE_SCHEMA_VERSION]);
98
111
  }
99
112
 
100
113
  export function readModelCache<TApi extends Api>(
@@ -102,13 +115,24 @@ export function readModelCache<TApi extends Api>(
102
115
  ttlMs: number,
103
116
  now: () => number,
104
117
  dbPath?: string,
118
+ expectedDiscoveryUserAgent?: string,
105
119
  ): CacheEntry<TApi> | null {
106
120
  try {
107
121
  const db = getDb(dbPath);
108
- const row = db.query<CacheRow, [string]>("SELECT * FROM model_cache WHERE provider_id = ?").get(providerId);
122
+ const select = db.query<CacheRow, [string]>("SELECT * FROM model_cache WHERE provider_id = ?");
123
+ let row: CacheRow | null;
124
+ try {
125
+ row = select.get(providerId);
126
+ } finally {
127
+ select.finalize();
128
+ }
109
129
  if (!row || row.version !== CACHE_SCHEMA_VERSION) {
110
130
  return null;
111
131
  }
132
+ // Invalidate cache if discovery User-Agent has changed
133
+ if (expectedDiscoveryUserAgent !== undefined && row.discovery_user_agent !== expectedDiscoveryUserAgent) {
134
+ return null;
135
+ }
112
136
  const models = JSON.parse(row.models) as Model<TApi>[];
113
137
  const ageMs = now() - row.updated_at;
114
138
  const fresh = Number.isFinite(ageMs) && ageMs >= 0 && ageMs <= ttlMs;
@@ -120,6 +144,7 @@ export function readModelCache<TApi extends Api>(
120
144
  staticFingerprint: row.static_fingerprint ?? "",
121
145
  dynamicModelIds: row.dynamic_model_ids === null ? undefined : (JSON.parse(row.dynamic_model_ids) as string[]),
122
146
  dynamicModelProvenance: row.dynamic_model_provenance ?? undefined,
147
+ discoveryUserAgent: row.discovery_user_agent ?? undefined,
123
148
  };
124
149
  } catch {
125
150
  return null;
@@ -135,12 +160,13 @@ export function writeModelCache<TApi extends Api>(
135
160
  dbPath?: string,
136
161
  dynamicModelIds?: readonly string[],
137
162
  dynamicModelProvenance?: string,
163
+ discoveryUserAgent?: string,
138
164
  ): void {
139
165
  try {
140
166
  const db = getDb(dbPath);
141
167
  db.run(
142
- `INSERT OR REPLACE INTO model_cache (provider_id, version, updated_at, authoritative, static_fingerprint, dynamic_model_ids, dynamic_model_provenance, models)
143
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
168
+ `INSERT OR REPLACE INTO model_cache (provider_id, version, updated_at, authoritative, static_fingerprint, dynamic_model_ids, dynamic_model_provenance, discovery_user_agent, models)
169
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
144
170
  [
145
171
  providerId,
146
172
  CACHE_SCHEMA_VERSION,
@@ -149,6 +175,7 @@ export function writeModelCache<TApi extends Api>(
149
175
  staticFingerprint,
150
176
  dynamicModelIds === undefined ? null : JSON.stringify(dynamicModelIds),
151
177
  dynamicModelProvenance ?? null,
178
+ discoveryUserAgent ?? null,
152
179
  JSON.stringify(models),
153
180
  ],
154
181
  );
@@ -166,11 +193,12 @@ export function insertModelCacheIfAbsent<TApi extends Api>(
166
193
  dbPath?: string,
167
194
  dynamicModelIds?: readonly string[],
168
195
  dynamicModelProvenance?: string,
196
+ discoveryUserAgent?: string,
169
197
  ): boolean {
170
198
  try {
171
199
  const result = getDb(dbPath).run(
172
- `INSERT INTO model_cache (provider_id, version, updated_at, authoritative, static_fingerprint, dynamic_model_ids, dynamic_model_provenance, models)
173
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
200
+ `INSERT INTO model_cache (provider_id, version, updated_at, authoritative, static_fingerprint, dynamic_model_ids, dynamic_model_provenance, discovery_user_agent, models)
201
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
174
202
  ON CONFLICT(provider_id) DO NOTHING`,
175
203
  [
176
204
  providerId,
@@ -180,6 +208,7 @@ export function insertModelCacheIfAbsent<TApi extends Api>(
180
208
  staticFingerprint,
181
209
  dynamicModelIds === undefined ? null : JSON.stringify(dynamicModelIds),
182
210
  dynamicModelProvenance ?? null,
211
+ discoveryUserAgent ?? null,
183
212
  JSON.stringify(models),
184
213
  ],
185
214
  );
@@ -195,6 +224,7 @@ export function updateModelCacheIfUnchanged<TApi extends Api>(
195
224
  expectedDynamicModelIds: readonly string[] | undefined,
196
225
  expectedDynamicModelProvenance: string | undefined,
197
226
  expectedModels: readonly Model<TApi>[],
227
+ expectedDiscoveryUserAgent: string | undefined,
198
228
  updatedAt: number,
199
229
  models: Model<TApi>[],
200
230
  authoritative: boolean,
@@ -202,30 +232,35 @@ export function updateModelCacheIfUnchanged<TApi extends Api>(
202
232
  dbPath?: string,
203
233
  dynamicModelIds?: readonly string[],
204
234
  dynamicModelProvenance?: string,
235
+ discoveryUserAgent?: string,
205
236
  ): boolean {
206
237
  try {
207
238
  const expectedIds = expectedDynamicModelIds === undefined ? null : JSON.stringify(expectedDynamicModelIds);
208
239
  const provenance = expectedDynamicModelProvenance ?? null;
240
+ const expectedUserAgent = expectedDiscoveryUserAgent ?? null;
209
241
  const nextIds = dynamicModelIds === undefined ? null : JSON.stringify(dynamicModelIds);
210
242
  const nextProvenance = dynamicModelProvenance ?? null;
211
243
  const expectedModelsJson = JSON.stringify(expectedModels);
212
244
  const result = getDb(dbPath).run(
213
245
  `UPDATE model_cache
214
246
  SET updated_at = ?, authoritative = ?, static_fingerprint = ?, dynamic_model_ids = ?,
215
- dynamic_model_provenance = ?, models = ?
247
+ dynamic_model_provenance = ?, discovery_user_agent = ?, models = ?
216
248
  WHERE provider_id = ? AND updated_at = ?
217
- AND dynamic_model_ids IS ? AND dynamic_model_provenance IS ? AND models = ?`,
249
+ AND dynamic_model_ids IS ? AND dynamic_model_provenance IS ?
250
+ AND discovery_user_agent IS ? AND models = ?`,
218
251
  [
219
252
  updatedAt,
220
253
  authoritative ? 1 : 0,
221
254
  staticFingerprint,
222
255
  nextIds,
223
256
  nextProvenance,
257
+ discoveryUserAgent ?? null,
224
258
  JSON.stringify(models),
225
259
  providerId,
226
260
  expectedUpdatedAt,
227
261
  expectedIds,
228
262
  provenance,
263
+ expectedUserAgent,
229
264
  expectedModelsJson,
230
265
  ],
231
266
  );
@@ -60,6 +60,8 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
60
60
  canPublishCache?: () => boolean;
61
61
  /** Credential-and-endpoint identity required to reuse dynamic catalog IDs. */
62
62
  cacheDynamicModelProvenance?: string;
63
+ /** Discovery User-Agent (e.g., for Antigravity): cache invalidates on change. */
64
+ discoveryUserAgent?: string;
63
65
  }
64
66
 
65
67
  /**
@@ -146,7 +148,13 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
146
148
  const provenance = options.cacheDynamicModelProvenance;
147
149
  const now = options.now ?? Date.now;
148
150
  const ttlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
149
- const legacyCache = readModelCache<TApi>(options.providerId, ttlMs, now, options.cacheDbPath);
151
+ const legacyCache = readModelCache<TApi>(
152
+ options.providerId,
153
+ ttlMs,
154
+ now,
155
+ options.cacheDbPath,
156
+ options.discoveryUserAgent,
157
+ );
150
158
  if (
151
159
  strategy !== "online-if-uncached" ||
152
160
  typeof options.fetchDynamicModels !== "function" ||
@@ -185,7 +193,7 @@ async function resolveProviderModelsUncoalesced<TApi extends Api = Api, TModelsD
185
193
  const staticModels = passModelList<TApi>(
186
194
  options.staticModels ?? getBundledModels(options.providerId as GeneratedProvider),
187
195
  );
188
- const cache = readModelCache<TApi>(options.providerId, ttlMs, now, dbPath);
196
+ const cache = readModelCache<TApi>(options.providerId, ttlMs, now, dbPath, options.discoveryUserAgent);
189
197
  const dynamicFetcher = options.fetchDynamicModels;
190
198
  const hasDynamicFetcher = typeof dynamicFetcher === "function";
191
199
  const cacheDynamicModelIdsCurrent =
@@ -262,6 +270,7 @@ async function resolveProviderModelsUncoalesced<TApi extends Api = Api, TModelsD
262
270
  dbPath,
263
271
  cache.dynamicModelIds,
264
272
  cache.dynamicModelProvenance,
273
+ options.discoveryUserAgent,
265
274
  );
266
275
  }
267
276
  return {
@@ -315,6 +324,7 @@ async function resolveProviderModelsUncoalesced<TApi extends Api = Api, TModelsD
315
324
  dbPath,
316
325
  dynamicModels.map(model => model.id),
317
326
  options.cacheDynamicModelProvenance,
327
+ options.discoveryUserAgent,
318
328
  );
319
329
  }
320
330
  } else {
@@ -322,10 +332,9 @@ async function resolveProviderModelsUncoalesced<TApi extends Api = Api, TModelsD
322
332
  // stale state remains visible while retry backoff still applies. Re-read
323
333
  // the current row and trust only that row's provenance and dynamic IDs,
324
334
  // not the initial cacheProvenanceMismatch snapshot. A concurrent writer
325
- // may have replaced the provider row; if the latest row is unbound or
326
- // belongs to another context, use no latest fallback and do not
327
- // overwrite or downgrade it.
328
- const latestCache = readModelCache<TApi>(options.providerId, ttlMs, now, dbPath);
335
+ // may have replaced the provider row. This reread filters mismatched
336
+ // discovery User-Agents, while the checks below enforce dynamic provenance.
337
+ const latestCache = readModelCache<TApi>(options.providerId, ttlMs, now, dbPath, options.discoveryUserAgent);
329
338
  const latestCacheMatchesCurrentContext =
330
339
  options.cacheDynamicModelProvenance === undefined
331
340
  ? latestCache !== null && latestCache.dynamicModelIds === undefined
@@ -353,6 +362,7 @@ async function resolveProviderModelsUncoalesced<TApi extends Api = Api, TModelsD
353
362
  latestCache.dynamicModelIds,
354
363
  latestCache.dynamicModelProvenance,
355
364
  latestCache.models,
365
+ latestCache.discoveryUserAgent,
356
366
  now(),
357
367
  snapshotModels,
358
368
  false,
@@ -360,6 +370,7 @@ async function resolveProviderModelsUncoalesced<TApi extends Api = Api, TModelsD
360
370
  dbPath,
361
371
  options.cacheDynamicModelProvenance === undefined ? undefined : [],
362
372
  options.cacheDynamicModelProvenance,
373
+ options.discoveryUserAgent,
363
374
  );
364
375
  if (!updated) cacheModels = [];
365
376
  } else if (latestCache == null) {
@@ -372,6 +383,7 @@ async function resolveProviderModelsUncoalesced<TApi extends Api = Api, TModelsD
372
383
  dbPath,
373
384
  options.cacheDynamicModelProvenance === undefined ? undefined : [],
374
385
  options.cacheDynamicModelProvenance,
386
+ options.discoveryUserAgent,
375
387
  );
376
388
  if (!inserted) cacheModels = [];
377
389
  }
@@ -1,3 +1,11 @@
1
1
  import type { Api, Model, ModelCost } from "./types";
2
2
  export declare function getOpenAIModelCost<TApi extends Api>(model: Model<TApi>, inputTokens: number): ModelCost | undefined;
3
+ export declare function getAnthropicModelCost<TApi extends Api>(model: Model<TApi>, inputTokens: number): ModelCost | undefined;
4
+ /**
5
+ * $/million rate for 1-hour cache writes, or undefined when the model has a single
6
+ * cache-write rate. Cost calculation applies it to `usage.cttl.ephemeral1h` and bills
7
+ * the remaining cache-write tokens at `ModelCost.cacheWrite`.
8
+ */
9
+ export declare function getAnthropicCacheWriteOneHourRate<TApi extends Api>(model: Model<TApi>, inputTokens: number): number | undefined;
10
+ export declare function applyAnthropicModelPricing<TApi extends Api>(model: Model<TApi>): void;
3
11
  export declare function applyOpenAIModelPricing<TApi extends Api>(model: Model<TApi>): void;
@@ -6,6 +6,17 @@ interface TieredPricing {
6
6
  }
7
7
 
8
8
  const LONG_CONTEXT_THRESHOLD = 272_000;
9
+ // Anthropic Haiku 5.5's direct Messages API charges five times the base rates
10
+ // when prompt input exceeds 100K tokens, including cached input.
11
+ // https://platform.claude.com/docs/en/models/haiku-5-5/overview#pricing
12
+ const ANTHROPIC_HAIKU_5_5_LONG_CONTEXT_PRICING: LongContextPricing = {
13
+ threshold: 100_000,
14
+ cost: { input: 0.5, output: 2.5, cacheRead: 0.05, cacheWrite: 0.625 },
15
+ };
16
+ // `ModelCost.cacheWrite` is the 5-minute cache-write rate. Haiku 5.5 charges 1-hour
17
+ // writes at twice the base input rate in each prompt-length tier ($0.20 up to 100K
18
+ // tokens, $1 above), and the provider defaults to `ttl: "1h"`.
19
+ const ANTHROPIC_HAIKU_5_5_CACHE_WRITE_1H = { standard: 0.2, longContext: 1 };
9
20
 
10
21
  const GPT_5_6_SOL_PRICING: TieredPricing = {
11
22
  cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 },
@@ -95,6 +106,82 @@ export function getOpenAIModelCost<TApi extends Api>(model: Model<TApi>, inputTo
95
106
  return inputTokens > pricing.longContextPricing.threshold ? pricing.longContextPricing.cost : pricing.cost;
96
107
  }
97
108
 
109
+ const BEDROCK_HAIKU_5_5_ID = /^(?:(?:au|eu|global|jp|us).)?anthropic.claude-haiku-5-5$/;
110
+ // Bedrock bills Haiku 5.5 prompts above 100K tokens at higher "Long Context" rates.
111
+ // AWS documents the threshold but the per-token rates only on its pricing page,
112
+ // which is not machine-readable; mirror Anthropic's published 5x multiplier on each
113
+ // inference profile's own base rates.
114
+ // https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-haiku-5-5.html#model-card-anthropic-claude-haiku-5-5-pricing
115
+ const BEDROCK_HAIKU_5_5_LONG_CONTEXT_MULTIPLIER = 5;
116
+
117
+ function getAnthropicLongContextPricing<TApi extends Api>(model: Model<TApi>): LongContextPricing | undefined {
118
+ if (model.provider === "anthropic" && model.api === "anthropic-messages" && model.id === "claude-haiku-5-5") {
119
+ return {
120
+ threshold: ANTHROPIC_HAIKU_5_5_LONG_CONTEXT_PRICING.threshold,
121
+ cost: { ...ANTHROPIC_HAIKU_5_5_LONG_CONTEXT_PRICING.cost },
122
+ };
123
+ }
124
+ if (
125
+ model.provider === "amazon-bedrock" &&
126
+ model.api === "bedrock-converse-stream" &&
127
+ BEDROCK_HAIKU_5_5_ID.test(model.id)
128
+ ) {
129
+ const m = BEDROCK_HAIKU_5_5_LONG_CONTEXT_MULTIPLIER;
130
+ const scale = (rate: number): number => Math.round(rate * m * 1e9) / 1e9;
131
+ return {
132
+ threshold: ANTHROPIC_HAIKU_5_5_LONG_CONTEXT_PRICING.threshold,
133
+ cost: {
134
+ input: scale(model.cost.input),
135
+ output: scale(model.cost.output),
136
+ cacheRead: scale(model.cost.cacheRead),
137
+ cacheWrite: scale(model.cost.cacheWrite),
138
+ },
139
+ };
140
+ }
141
+ return undefined;
142
+ }
143
+
144
+ export function getAnthropicModelCost<TApi extends Api>(
145
+ model: Model<TApi>,
146
+ inputTokens: number,
147
+ ): ModelCost | undefined {
148
+ const pricing = getAnthropicLongContextPricing(model);
149
+ if (!pricing || inputTokens <= pricing.threshold) return undefined;
150
+ return pricing.cost;
151
+ }
152
+
153
+ /**
154
+ * $/million rate for 1-hour cache writes, or undefined when the model has a single
155
+ * cache-write rate. Cost calculation applies it to `usage.cttl.ephemeral1h` and bills
156
+ * the remaining cache-write tokens at `ModelCost.cacheWrite`.
157
+ */
158
+ export function getAnthropicCacheWriteOneHourRate<TApi extends Api>(
159
+ model: Model<TApi>,
160
+ inputTokens: number,
161
+ ): number | undefined {
162
+ const aboveThreshold = inputTokens > ANTHROPIC_HAIKU_5_5_LONG_CONTEXT_PRICING.threshold;
163
+ if (model.provider === "anthropic" && model.api === "anthropic-messages" && model.id === "claude-haiku-5-5") {
164
+ return aboveThreshold
165
+ ? ANTHROPIC_HAIKU_5_5_CACHE_WRITE_1H.longContext
166
+ : ANTHROPIC_HAIKU_5_5_CACHE_WRITE_1H.standard;
167
+ }
168
+ if (
169
+ model.provider === "amazon-bedrock" &&
170
+ model.api === "bedrock-converse-stream" &&
171
+ BEDROCK_HAIKU_5_5_ID.test(model.id)
172
+ ) {
173
+ // Same structure as direct: 1-hour writes cost twice the tier's input rate.
174
+ const multiplier = aboveThreshold ? BEDROCK_HAIKU_5_5_LONG_CONTEXT_MULTIPLIER : 1;
175
+ return Math.round(2 * model.cost.input * multiplier * 1e9) / 1e9;
176
+ }
177
+ return undefined;
178
+ }
179
+
180
+ export function applyAnthropicModelPricing<TApi extends Api>(model: Model<TApi>): void {
181
+ const pricing = getAnthropicLongContextPricing(model);
182
+ if (pricing) model.longContextPricing = pricing;
183
+ }
184
+
98
185
  export function applyOpenAIModelPricing<TApi extends Api>(model: Model<TApi>): void {
99
186
  if (model.provider !== "openai" && model.provider !== "openai-codex") {
100
187
  return;
@@ -87,12 +87,23 @@ export declare function mapEffortToAnthropicAdaptiveEffort<TApi extends Api>(mod
87
87
  * - Thinking content is omitted by default (needs display: "summarized")
88
88
  */
89
89
  export declare function hasOpus47ApiRestrictions(modelId: string): boolean;
90
+ /**
91
+ * Haiku 5+ enables adaptive thinking when the `thinking` field is omitted.
92
+ * An explicit user-facing Off choice must therefore send `type: "disabled"`.
93
+ */
94
+ export declare function requiresAnthropicThinkingDisableWhenOff(modelId: string): boolean;
95
+ /**
96
+ * Haiku 5+ and Opus 4.7+ reject sampling parameters on the Anthropic Messages API.
97
+ * https://platform.claude.com/docs/en/models/haiku-5-5/migration-guide#remove-sampling-parameters
98
+ */
99
+ export declare function hasAnthropicModelSamplingRestrictions(modelId: string): boolean;
90
100
  /**
91
101
  * Adaptive thinking `display` is supported starting with Anthropic Opus 4.7.
92
102
  * Older adaptive-thinking models (Opus 4.6, Sonnet 4.6+) reject the field.
93
103
  * Fable (5+) postdates Opus 4.7, accepts `display`, and defaults it to
94
104
  * "omitted" — thinking tokens are billed but no content streams back — so it
95
105
  * must opt in like Opus 4.7+ (issue #2791).
106
+ * Haiku 5+ also accepts `display` and defaults it to "omitted".
96
107
  *
97
108
  * Shares `hasOpus47ApiRestrictions` version parsing on purpose: the two
98
109
  * predicates describe the same API generation, and a private `claude-opus-(\d+)-(\d+)`
@@ -5,7 +5,7 @@ import {
5
5
  isCodexGpt56Tier,
6
6
  isCodexProductTransport,
7
7
  } from "./context-cap-policy";
8
- import { applyOpenAIModelPricing } from "./model-pricing";
8
+ import { applyAnthropicModelPricing, applyOpenAIModelPricing } from "./model-pricing";
9
9
  import {
10
10
  isAuditedOpenAIReasoningTransport,
11
11
  parseDirectXaiReasoningEffortGeneration,
@@ -56,6 +56,17 @@ const DEFAULT_REASONING_EFFORTS_WITH_XHIGH_AND_MAX: readonly Effort[] = [
56
56
  Effort.XHigh,
57
57
  Effort.Max,
58
58
  ];
59
+ // Haiku 5.5 supports low, medium, high, xhigh, and max on both Anthropic
60
+ // Messages and Bedrock Converse; neither contract lists minimal.
61
+ // https://platform.claude.com/docs/en/build-with-claude/effort#effort-levels
62
+ // https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-haiku-5-5.html
63
+ const ANTHROPIC_HAIKU_5_5_EFFORTS: readonly Effort[] = [
64
+ Effort.Low,
65
+ Effort.Medium,
66
+ Effort.High,
67
+ Effort.XHigh,
68
+ Effort.Max,
69
+ ];
59
70
  const GEMINI_3_PRO_EFFORTS: readonly Effort[] = [Effort.Low, Effort.High];
60
71
  const GEMINI_3_FLASH_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High];
61
72
  // Gemini 3.7 Flash dropped `minimal`; the official API returns an error for it.
@@ -95,8 +106,8 @@ type SemVer = {
95
106
  patch: number;
96
107
  };
97
108
 
98
- type GeminiKind = "pro" | "flash";
99
- type AnthropicKind = "opus" | "sonnet" | "fable";
109
+ type GeminiKind = "pro" | "flash" | "argon";
110
+ type AnthropicKind = "opus" | "sonnet" | "fable" | "haiku";
100
111
  type OpenAIVariant =
101
112
  | "astra"
102
113
  | "base"
@@ -328,6 +339,23 @@ export function modelSupportsReasoningControl<TApi extends Api>(
328
339
  export function applyGeneratedModelPolicies(models: ApiModel<Api>[]): void {
329
340
  for (let index = 0; index < models.length; index++) {
330
341
  const source = models[index]!;
342
+ const inputCost = nonNegativeFinitePrice(source.cost.input);
343
+ const outputCost = nonNegativeFinitePrice(source.cost.output);
344
+ const cacheReadCost = nonNegativeFinitePrice(source.cost.cacheRead);
345
+ const cacheWriteCost = nonNegativeFinitePrice(source.cost.cacheWrite);
346
+ if (
347
+ inputCost !== source.cost.input ||
348
+ outputCost !== source.cost.output ||
349
+ cacheReadCost !== source.cost.cacheRead ||
350
+ cacheWriteCost !== source.cost.cacheWrite
351
+ ) {
352
+ source.cost = {
353
+ input: inputCost,
354
+ output: outputCost,
355
+ cacheRead: cacheReadCost,
356
+ cacheWrite: cacheWriteCost,
357
+ };
358
+ }
331
359
  if (source.provider === "xai" && (source.id === "grok-4.5" || source.id === "grok-4.6")) {
332
360
  source.reasoning = true;
333
361
  }
@@ -359,6 +387,10 @@ export function applyGeneratedModelPolicies(models: ApiModel<Api>[]): void {
359
387
  }
360
388
  }
361
389
 
390
+ function nonNegativeFinitePrice(value: number): number {
391
+ return Number.isFinite(value) && value >= 0 ? value : 0;
392
+ }
393
+
362
394
  /**
363
395
  * Link OpenAI model variants to their context promotion targets.
364
396
  *
@@ -517,12 +549,34 @@ export function hasOpus47ApiRestrictions(modelId: string): boolean {
517
549
  return semverGte(parsed.version, "4.7") && parsed.kind === "opus";
518
550
  }
519
551
 
552
+ function isAnthropicHaikuAdaptiveModel(modelId: string): boolean {
553
+ const parsed = parseAnthropicModel(getCanonicalModelId(modelId));
554
+ return parsed?.kind === "haiku" && semverGte(parsed.version, "5.0");
555
+ }
556
+
557
+ /**
558
+ * Haiku 5+ enables adaptive thinking when the `thinking` field is omitted.
559
+ * An explicit user-facing Off choice must therefore send `type: "disabled"`.
560
+ */
561
+ export function requiresAnthropicThinkingDisableWhenOff(modelId: string): boolean {
562
+ return isAnthropicHaikuAdaptiveModel(modelId);
563
+ }
564
+
565
+ /**
566
+ * Haiku 5+ and Opus 4.7+ reject sampling parameters on the Anthropic Messages API.
567
+ * https://platform.claude.com/docs/en/models/haiku-5-5/migration-guide#remove-sampling-parameters
568
+ */
569
+ export function hasAnthropicModelSamplingRestrictions(modelId: string): boolean {
570
+ return hasOpus47ApiRestrictions(modelId) || isAnthropicHaikuAdaptiveModel(modelId);
571
+ }
572
+
520
573
  /**
521
574
  * Adaptive thinking `display` is supported starting with Anthropic Opus 4.7.
522
575
  * Older adaptive-thinking models (Opus 4.6, Sonnet 4.6+) reject the field.
523
576
  * Fable (5+) postdates Opus 4.7, accepts `display`, and defaults it to
524
577
  * "omitted" — thinking tokens are billed but no content streams back — so it
525
578
  * must opt in like Opus 4.7+ (issue #2791).
579
+ * Haiku 5+ also accepts `display` and defaults it to "omitted".
526
580
  *
527
581
  * Shares `hasOpus47ApiRestrictions` version parsing on purpose: the two
528
582
  * predicates describe the same API generation, and a private `claude-opus-(\d+)-(\d+)`
@@ -533,7 +587,7 @@ export function hasOpus47ApiRestrictions(modelId: string): boolean {
533
587
  */
534
588
  export function supportsAnthropicAdaptiveThinkingDisplay(modelId: string): boolean {
535
589
  if (/claude-fable-\d/.test(modelId)) return true;
536
- return hasOpus47ApiRestrictions(modelId);
590
+ return hasOpus47ApiRestrictions(modelId) || isAnthropicHaikuAdaptiveModel(modelId);
537
591
  }
538
592
 
539
593
  function anthropicModelHasRealXHighEffort<TApi extends Api>(model: ApiModel<TApi>): boolean {
@@ -555,6 +609,7 @@ function anthropicModelHasRealXHighEffort<TApi extends Api>(model: ApiModel<TApi
555
609
 
556
610
  function applyGeneratedModelPolicy(model: ApiModel<Api>): void {
557
611
  applyOpenAIModelPricing(model);
612
+ applyAnthropicModelPricing(model);
558
613
  const copilotLimits = model.provider === "github-copilot" ? COPILOT_GENERATED_LIMITS[model.id] : undefined;
559
614
  if (copilotLimits) {
560
615
  model.contextWindow = copilotLimits.contextWindow;
@@ -729,6 +784,17 @@ function applyGeneratedModelPolicy(model: ApiModel<Api>): void {
729
784
  ) {
730
785
  model.contextWindow = 1_000_000;
731
786
  }
787
+ // Google Gemini 4 Argon supports up to 65,536 output tokens across provider
788
+ // aliases. Strip catalog provider prefixes and OpenRouter's batch suffix.
789
+ // https://ai.google.dev/gemini-api/docs/models/gemini-4-argon
790
+ const canonicalId = model.id
791
+ .toLowerCase()
792
+ .split("/")
793
+ .at(-1)
794
+ ?.replace(/:batch$/, "");
795
+ if (canonicalId === "gemini-4-argon") {
796
+ model.maxTokens = Math.min(model.maxTokens, 65_536);
797
+ }
732
798
  }
733
799
 
734
800
  function scrubGeneratedModelName(name: string): string {
@@ -847,6 +913,19 @@ function inferDefaultEffort<TApi extends Api>(model: ApiModel<TApi>, parsedModel
847
913
  if (model.provider === "omlx") {
848
914
  return Effort.Medium;
849
915
  }
916
+ if (
917
+ parsedModel.family === "anthropic" &&
918
+ parsedModel.kind === "haiku" &&
919
+ ((model.provider === "anthropic" &&
920
+ model.api === "anthropic-messages" &&
921
+ semverGte(parsedModel.version, "5.0")) ||
922
+ (model.provider === "amazon-bedrock" &&
923
+ model.api === "bedrock-converse-stream" &&
924
+ semverGte(parsedModel.version, "5.5")))
925
+ ) {
926
+ // Haiku 5.5 defaults to medium adaptive effort instead of the global high.
927
+ return Effort.Medium;
928
+ }
850
929
  return undefined;
851
930
  }
852
931
 
@@ -979,6 +1058,8 @@ function inferGeminiSupportedEfforts(model: GeminiModel): readonly Effort[] {
979
1058
  if (model.kind === "pro") {
980
1059
  return GEMINI_3_PRO_EFFORTS;
981
1060
  }
1061
+ // Gemini 3.7+ Flash and Gemini 4 Argon accept low/medium/high only.
1062
+ // https://ai.google.dev/gemini-api/docs/models/gemini-4-argon
982
1063
  if (semverGte(model.version, "3.7")) {
983
1064
  return GEMINI_3_7_FLASH_EFFORTS;
984
1065
  }
@@ -993,6 +1074,9 @@ function inferAnthropicSupportedEfforts<TApi extends Api>(
993
1074
  (model.api === "anthropic-messages" || model.api === "bedrock-converse-stream") &&
994
1075
  semverGte(parsedModel.version, "4.6")
995
1076
  ) {
1077
+ if (parsedModel.kind === "haiku" && semverGte(parsedModel.version, "5.5")) {
1078
+ return ANTHROPIC_HAIKU_5_5_EFFORTS;
1079
+ }
996
1080
  if (parsedModel.kind === "fable") {
997
1081
  // Fable exposes Anthropic's Messages-only xhigh preset; Bedrock
998
1082
  // Converse lacks it (same split as Opus 4.7+ below).
@@ -1054,11 +1138,7 @@ function inferThinkingControlMode<TApi extends Api>(
1054
1138
  case "google-generative-ai":
1055
1139
  case "google-gemini-cli":
1056
1140
  case "google-vertex":
1057
- return parsedModel.family === "gemini" &&
1058
- semverGte(parsedModel.version, "3.0") &&
1059
- parsedModel.version.major === 3
1060
- ? "google-level"
1061
- : "budget";
1141
+ return parsedModel.family === "gemini" && semverGte(parsedModel.version, "3.0") ? "google-level" : "budget";
1062
1142
 
1063
1143
  case "anthropic-messages":
1064
1144
  if (parsedModel.family === "anthropic") {
@@ -1075,7 +1155,7 @@ function inferThinkingControlMode<TApi extends Api>(
1075
1155
  if (parsedModel.family === "anthropic") {
1076
1156
  if (
1077
1157
  semverGte(parsedModel.version, "4.6") &&
1078
- (parsedModel.kind === "opus" || parsedModel.kind === "fable")
1158
+ (parsedModel.kind === "opus" || parsedModel.kind === "fable" || parsedModel.kind === "haiku")
1079
1159
  ) {
1080
1160
  return "anthropic-adaptive";
1081
1161
  }
@@ -1104,7 +1184,7 @@ function parseGeminiModel(modelId: string): GeminiModel | null {
1104
1184
  if (modelId.endsWith(GEMINI_SUFFIX)) {
1105
1185
  modelId = modelId.slice(0, -GEMINI_SUFFIX.length);
1106
1186
  }
1107
- const match = /gemini-(\d+(?:\.\d+){0,2})-(pro|flash)\b/.exec(modelId);
1187
+ const match = /gemini-(\d+(?:\.\d+){0,2})-(pro|flash|argon)\b/.exec(modelId);
1108
1188
  if (!match) {
1109
1189
  return null;
1110
1190
  }
@@ -1116,7 +1196,7 @@ function parseGeminiModel(modelId: string): GeminiModel | null {
1116
1196
  }
1117
1197
 
1118
1198
  function parseAnthropicModel(modelId: string): AnthropicModel | null {
1119
- const match = /claude-(opus|sonnet|fable)-(\d{1,2}(?:[.-]\d{1,2}){0,2})\b/.exec(modelId);
1199
+ const match = /claude-(opus|sonnet|fable|haiku)-(\d{1,2}(?:[.-]\d{1,2}){0,2})\b/.exec(modelId);
1120
1200
  if (!match) {
1121
1201
  return null;
1122
1202
  }
@@ -1124,6 +1204,11 @@ function parseAnthropicModel(modelId: string): AnthropicModel | null {
1124
1204
  if (!version) {
1125
1205
  return null;
1126
1206
  }
1207
+ // Haiku is recognized from generation 5 only. Haiku 4.5 predates the
1208
+ // adaptive-thinking policy and keeps the generic (unparsed) budget ladder.
1209
+ if (match[1] === "haiku" && !semverGte(version, "5.0")) {
1210
+ return null;
1211
+ }
1127
1212
  return { family: "anthropic", kind: match[1] as AnthropicKind, version };
1128
1213
  }
1129
1214