@earendil-works/pi-ai 0.80.6 → 0.80.8
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/README.md +79 -24
- package/dist/api/anthropic-messages.d.ts.map +1 -1
- package/dist/api/anthropic-messages.js +95 -46
- package/dist/api/anthropic-messages.js.map +1 -1
- package/dist/api/bedrock-converse-stream.d.ts.map +1 -1
- package/dist/api/bedrock-converse-stream.js +14 -7
- package/dist/api/bedrock-converse-stream.js.map +1 -1
- package/dist/api/lazy.d.ts.map +1 -1
- package/dist/api/lazy.js +9 -10
- package/dist/api/lazy.js.map +1 -1
- package/dist/api/openai-codex-responses.d.ts +1 -0
- package/dist/api/openai-codex-responses.d.ts.map +1 -1
- package/dist/api/openai-codex-responses.js +9 -5
- package/dist/api/openai-codex-responses.js.map +1 -1
- package/dist/api/openai-completions.d.ts.map +1 -1
- package/dist/api/openai-completions.js +12 -3
- package/dist/api/openai-completions.js.map +1 -1
- package/dist/api/openai-responses-shared.d.ts +3 -1
- package/dist/api/openai-responses-shared.d.ts.map +1 -1
- package/dist/api/openai-responses-shared.js +51 -0
- package/dist/api/openai-responses-shared.js.map +1 -1
- package/dist/api/openai-responses.d.ts +1 -0
- package/dist/api/openai-responses.d.ts.map +1 -1
- package/dist/api/openai-responses.js +26 -8
- package/dist/api/openai-responses.js.map +1 -1
- package/dist/api/pi-messages.d.ts +97 -0
- package/dist/api/pi-messages.d.ts.map +1 -0
- package/dist/api/pi-messages.js +309 -0
- package/dist/api/pi-messages.js.map +1 -0
- package/dist/api/pi-messages.lazy.d.ts +3 -0
- package/dist/api/pi-messages.lazy.d.ts.map +1 -0
- package/dist/api/pi-messages.lazy.js +3 -0
- package/dist/api/pi-messages.lazy.js.map +1 -0
- package/dist/auth/credential-store.d.ts +2 -1
- package/dist/auth/credential-store.d.ts.map +1 -1
- package/dist/auth/credential-store.js +3 -0
- package/dist/auth/credential-store.js.map +1 -1
- package/dist/auth/helpers.d.ts.map +1 -1
- package/dist/auth/helpers.js +3 -3
- package/dist/auth/helpers.js.map +1 -1
- package/dist/auth/oauth/anthropic.d.ts +9 -0
- package/dist/auth/oauth/anthropic.d.ts.map +1 -0
- package/dist/{utils → auth}/oauth/anthropic.js +51 -116
- package/dist/auth/oauth/anthropic.js.map +1 -0
- package/dist/auth/oauth/device-code.d.ts.map +1 -0
- package/dist/auth/oauth/device-code.js.map +1 -0
- package/dist/auth/oauth/github-copilot.d.ts +6 -0
- package/dist/auth/oauth/github-copilot.d.ts.map +1 -0
- package/dist/{utils → auth}/oauth/github-copilot.js +18 -77
- package/dist/auth/oauth/github-copilot.js.map +1 -0
- package/dist/auth/oauth/load.d.ts +23 -0
- package/dist/auth/oauth/load.d.ts.map +1 -0
- package/dist/auth/oauth/load.js +49 -0
- package/dist/auth/oauth/load.js.map +1 -0
- package/dist/auth/oauth/oauth-page.d.ts.map +1 -0
- package/dist/auth/oauth/oauth-page.js.map +1 -0
- package/dist/auth/oauth/openai-codex.d.ts +9 -0
- package/dist/auth/oauth/openai-codex.d.ts.map +1 -0
- package/dist/{utils → auth}/oauth/openai-codex.js +61 -155
- package/dist/auth/oauth/openai-codex.js.map +1 -0
- package/dist/auth/oauth/pkce.d.ts.map +1 -0
- package/dist/auth/oauth/pkce.js.map +1 -0
- package/dist/auth/oauth/radius.d.ts +16 -0
- package/dist/auth/oauth/radius.d.ts.map +1 -0
- package/dist/auth/oauth/radius.js +318 -0
- package/dist/auth/oauth/radius.js.map +1 -0
- package/dist/auth/oauth/xai.d.ts +6 -0
- package/dist/auth/oauth/xai.d.ts.map +1 -0
- package/dist/auth/oauth/xai.js +184 -0
- package/dist/auth/oauth/xai.js.map +1 -0
- package/dist/auth/resolve.d.ts +2 -4
- package/dist/auth/resolve.d.ts.map +1 -1
- package/dist/auth/resolve.js +9 -7
- package/dist/auth/resolve.js.map +1 -1
- package/dist/auth/types.d.ts +47 -10
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/auth/types.js.map +1 -1
- package/dist/bun-oauth.d.ts +3 -0
- package/dist/bun-oauth.d.ts.map +1 -0
- package/dist/bun-oauth.js +17 -0
- package/dist/bun-oauth.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +58 -76
- package/dist/cli.js.map +1 -1
- package/dist/compat/extension-oauth-types.d.ts +39 -0
- package/dist/compat/extension-oauth-types.d.ts.map +1 -0
- package/dist/compat/extension-oauth-types.js +2 -0
- package/dist/compat/extension-oauth-types.js.map +1 -0
- package/dist/compat.d.ts +2 -0
- package/dist/compat.d.ts.map +1 -1
- package/dist/compat.js +25 -8
- package/dist/compat.js.map +1 -1
- package/dist/env-api-keys.d.ts.map +1 -1
- package/dist/env-api-keys.js +1 -0
- package/dist/env-api-keys.js.map +1 -1
- package/dist/images-models.d.ts +4 -2
- package/dist/images-models.d.ts.map +1 -1
- package/dist/images-models.js +5 -4
- package/dist/images-models.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/models-store.d.ts +25 -0
- package/dist/models-store.d.ts.map +1 -0
- package/dist/models-store.js +14 -0
- package/dist/models-store.js.map +1 -0
- package/dist/models.d.ts +66 -30
- package/dist/models.d.ts.map +1 -1
- package/dist/models.generated.d.ts +570 -163
- package/dist/models.generated.d.ts.map +1 -1
- package/dist/models.js +214 -29
- package/dist/models.js.map +1 -1
- package/dist/oauth.d.ts +2 -1
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +1 -1
- package/dist/oauth.js.map +1 -1
- package/dist/providers/all.d.ts +11 -6
- package/dist/providers/all.d.ts.map +1 -1
- package/dist/providers/all.js +3 -0
- package/dist/providers/all.js.map +1 -1
- package/dist/providers/amazon-bedrock.d.ts.map +1 -1
- package/dist/providers/amazon-bedrock.js +51 -6
- package/dist/providers/amazon-bedrock.js.map +1 -1
- package/dist/providers/amazon-bedrock.models.d.ts +60 -0
- package/dist/providers/amazon-bedrock.models.d.ts.map +1 -1
- package/dist/providers/amazon-bedrock.models.js +54 -0
- package/dist/providers/amazon-bedrock.models.js.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +1 -1
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/azure-openai-responses.models.d.ts +17 -0
- package/dist/providers/azure-openai-responses.models.d.ts.map +1 -1
- package/dist/providers/azure-openai-responses.models.js +17 -0
- package/dist/providers/azure-openai-responses.models.js.map +1 -1
- package/dist/providers/cerebras.models.d.ts.map +1 -1
- package/dist/providers/cerebras.models.js +1 -1
- package/dist/providers/cerebras.models.js.map +1 -1
- package/dist/providers/cloudflare-ai-gateway.d.ts.map +1 -1
- package/dist/providers/cloudflare-ai-gateway.js +4 -3
- package/dist/providers/cloudflare-ai-gateway.js.map +1 -1
- package/dist/providers/cloudflare-ai-gateway.models.d.ts +92 -0
- package/dist/providers/cloudflare-ai-gateway.models.d.ts.map +1 -1
- package/dist/providers/cloudflare-ai-gateway.models.js +72 -0
- package/dist/providers/cloudflare-ai-gateway.models.js.map +1 -1
- package/dist/providers/cloudflare-auth.d.ts.map +1 -1
- package/dist/providers/cloudflare-auth.js +22 -26
- package/dist/providers/cloudflare-auth.js.map +1 -1
- package/dist/providers/cloudflare-stream.d.ts +8 -0
- package/dist/providers/cloudflare-stream.d.ts.map +1 -0
- package/dist/providers/cloudflare-stream.js +21 -0
- package/dist/providers/cloudflare-stream.js.map +1 -0
- package/dist/providers/cloudflare-workers-ai.d.ts.map +1 -1
- package/dist/providers/cloudflare-workers-ai.js +2 -1
- package/dist/providers/cloudflare-workers-ai.js.map +1 -1
- package/dist/providers/github-copilot.d.ts.map +1 -1
- package/dist/providers/github-copilot.js +11 -1
- package/dist/providers/github-copilot.js.map +1 -1
- package/dist/providers/github-copilot.models.d.ts +93 -6
- package/dist/providers/github-copilot.models.d.ts.map +1 -1
- package/dist/providers/github-copilot.models.js +60 -3
- package/dist/providers/github-copilot.models.js.map +1 -1
- package/dist/providers/google-vertex.d.ts.map +1 -1
- package/dist/providers/google-vertex.js +56 -5
- package/dist/providers/google-vertex.js.map +1 -1
- package/dist/providers/kimi-coding.models.d.ts +20 -0
- package/dist/providers/kimi-coding.models.d.ts.map +1 -1
- package/dist/providers/kimi-coding.models.js +18 -0
- package/dist/providers/kimi-coding.models.js.map +1 -1
- package/dist/providers/openai-codex.d.ts.map +1 -1
- package/dist/providers/openai-codex.js +1 -1
- package/dist/providers/openai-codex.js.map +1 -1
- package/dist/providers/openai-codex.models.d.ts +18 -0
- package/dist/providers/openai-codex.models.d.ts.map +1 -1
- package/dist/providers/openai-codex.models.js +6 -0
- package/dist/providers/openai-codex.models.js.map +1 -1
- package/dist/providers/openai.models.d.ts +38 -0
- package/dist/providers/openai.models.d.ts.map +1 -1
- package/dist/providers/openai.models.js +24 -0
- package/dist/providers/openai.models.js.map +1 -1
- package/dist/providers/opencode.models.d.ts +123 -0
- package/dist/providers/opencode.models.d.ts.map +1 -1
- package/dist/providers/opencode.models.js +74 -1
- package/dist/providers/opencode.models.js.map +1 -1
- package/dist/providers/openrouter.models.d.ts +28 -43
- package/dist/providers/openrouter.models.d.ts.map +1 -1
- package/dist/providers/openrouter.models.js +186 -203
- package/dist/providers/openrouter.models.js.map +1 -1
- package/dist/providers/radius-config.d.ts +26 -0
- package/dist/providers/radius-config.d.ts.map +1 -0
- package/dist/providers/radius-config.js +59 -0
- package/dist/providers/radius-config.js.map +1 -0
- package/dist/providers/radius.d.ts +9 -0
- package/dist/providers/radius.d.ts.map +1 -0
- package/dist/providers/radius.js +54 -0
- package/dist/providers/radius.js.map +1 -0
- package/dist/providers/vercel-ai-gateway.models.d.ts +71 -106
- package/dist/providers/vercel-ai-gateway.models.d.ts.map +1 -1
- package/dist/providers/vercel-ai-gateway.models.js +89 -136
- package/dist/providers/vercel-ai-gateway.models.js.map +1 -1
- package/dist/providers/xai.d.ts +1 -1
- package/dist/providers/xai.d.ts.map +1 -1
- package/dist/providers/xai.js +11 -3
- package/dist/providers/xai.js.map +1 -1
- package/dist/providers/xai.models.d.ts +6 -4
- package/dist/providers/xai.models.d.ts.map +1 -1
- package/dist/providers/xai.models.js +3 -2
- package/dist/providers/xai.models.js.map +1 -1
- package/dist/types.d.ts +25 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/deferred-tools.d.ts +9 -0
- package/dist/utils/deferred-tools.d.ts.map +1 -0
- package/dist/utils/deferred-tools.js +36 -0
- package/dist/utils/deferred-tools.js.map +1 -0
- package/dist/utils/estimate.d.ts.map +1 -1
- package/dist/utils/estimate.js +18 -5
- package/dist/utils/estimate.js.map +1 -1
- package/package.json +6 -1
- package/dist/utils/oauth/anthropic.d.ts +0 -27
- package/dist/utils/oauth/anthropic.d.ts.map +0 -1
- package/dist/utils/oauth/anthropic.js.map +0 -1
- package/dist/utils/oauth/device-code.d.ts.map +0 -1
- package/dist/utils/oauth/device-code.js.map +0 -1
- package/dist/utils/oauth/github-copilot.d.ts +0 -32
- package/dist/utils/oauth/github-copilot.d.ts.map +0 -1
- package/dist/utils/oauth/github-copilot.js.map +0 -1
- package/dist/utils/oauth/index.d.ts +0 -58
- package/dist/utils/oauth/index.d.ts.map +0 -1
- package/dist/utils/oauth/index.js +0 -122
- package/dist/utils/oauth/index.js.map +0 -1
- package/dist/utils/oauth/load.d.ts +0 -5
- package/dist/utils/oauth/load.d.ts.map +0 -1
- package/dist/utils/oauth/load.js +0 -22
- package/dist/utils/oauth/load.js.map +0 -1
- package/dist/utils/oauth/oauth-page.d.ts.map +0 -1
- package/dist/utils/oauth/oauth-page.js.map +0 -1
- package/dist/utils/oauth/openai-codex.d.ts +0 -45
- package/dist/utils/oauth/openai-codex.d.ts.map +0 -1
- package/dist/utils/oauth/openai-codex.js.map +0 -1
- package/dist/utils/oauth/pkce.d.ts.map +0 -1
- package/dist/utils/oauth/pkce.js.map +0 -1
- package/dist/utils/oauth/types.d.ts +0 -64
- package/dist/utils/oauth/types.d.ts.map +0 -1
- package/dist/utils/oauth/types.js +0 -2
- package/dist/utils/oauth/types.js.map +0 -1
- /package/dist/{utils → auth}/oauth/device-code.d.ts +0 -0
- /package/dist/{utils → auth}/oauth/device-code.js +0 -0
- /package/dist/{utils → auth}/oauth/oauth-page.d.ts +0 -0
- /package/dist/{utils → auth}/oauth/oauth-page.js +0 -0
- /package/dist/{utils → auth}/oauth/pkce.d.ts +0 -0
- /package/dist/{utils → auth}/oauth/pkce.js +0 -0
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ Unified LLM API with provider collections, automatic auth resolution, token and
|
|
|
17
17
|
- [Dynamic Providers](#dynamic-providers)
|
|
18
18
|
- [Auth](#auth)
|
|
19
19
|
- [How Auth Resolves](#how-auth-resolves)
|
|
20
|
+
- [Transforming Request Headers](#transforming-request-headers)
|
|
20
21
|
- [Credential Store](#credential-store)
|
|
21
22
|
- [Environment Variables](#environment-variables)
|
|
22
23
|
- [Tools](#tools)
|
|
@@ -334,18 +335,45 @@ await models.complete(model, context);
|
|
|
334
335
|
await models.complete(model, context, { apiKey: 'sk-explicit' });
|
|
335
336
|
```
|
|
336
337
|
|
|
337
|
-
You can inspect resolution without making a request
|
|
338
|
+
You can inspect resolution without making a request. Pass a provider ID for provider-scoped auth, or a model to include its static `model.headers`:
|
|
338
339
|
|
|
339
340
|
```typescript
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
341
|
+
const providerAuth = await models.getAuth(model.provider);
|
|
342
|
+
const modelAuth = await models.getAuth(model);
|
|
343
|
+
|
|
344
|
+
if (modelAuth) {
|
|
345
|
+
console.log(`configured via ${modelAuth.source}`); // e.g. "ANTHROPIC_API_KEY", "OAuth", "stored credential"
|
|
346
|
+
console.log(modelAuth.auth.headers); // Provider auth headers + model.headers
|
|
343
347
|
} else {
|
|
344
348
|
console.log('not configured');
|
|
345
349
|
}
|
|
346
350
|
```
|
|
347
351
|
|
|
348
|
-
`getAuth()` resolves `undefined` for unconfigured providers and rejects with `ModelsError` when something is actually broken (`"oauth"`: token refresh failed, credential preserved for re-login; `"auth"`: key resolution or credential store failure). Request paths surface the same failures as stream errors.
|
|
352
|
+
Both overloads resolve credentials, refresh expired OAuth when necessary, and may return an auth-derived `apiKey`, `headers`, or `baseUrl`. `getAuth()` resolves `undefined` for unconfigured providers and rejects with `ModelsError` when something is actually broken (`"oauth"`: token refresh failed, credential preserved for re-login; `"auth"`: key resolution or credential store failure). Request paths surface the same failures as stream errors.
|
|
353
|
+
|
|
354
|
+
### Transforming Request Headers
|
|
355
|
+
|
|
356
|
+
`Models.stream()`, `complete()`, `streamSimple()`, and `completeSimple()` accept a Models-only `transformHeaders` option. It runs once after provider auth, `model.headers`, and explicit `options.headers` have been merged, but before provider dispatch:
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
const response = await models.completeSimple(model, context, {
|
|
360
|
+
headers: { "X-Client": "my-app" },
|
|
361
|
+
transformHeaders: async (headers) => ({
|
|
362
|
+
...headers,
|
|
363
|
+
"X-Request-ID": crypto.randomUUID(),
|
|
364
|
+
}),
|
|
365
|
+
});
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
The ordering is:
|
|
369
|
+
|
|
370
|
+
```text
|
|
371
|
+
provider auth headers -> model.headers -> explicit options.headers -> transformHeaders -> Provider.stream*()
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Header names are merged case-insensitively. Explicit headers override auth/model headers, and the transform has final control; returning `null` for a header suppresses lower-level defaults that support deletion.
|
|
375
|
+
|
|
376
|
+
`transformHeaders` belongs to `Models`, not `Provider`. A `Models` implementation must consume it and remove it before calling `Provider.stream*()`. Provider implementations continue receiving ordinary `ApiStreamOptions` or `SimpleStreamOptions` and never handle the transform themselves. Use this option instead of calling `getAuth(model)` before `stream*()`, which would resolve request auth twice.
|
|
349
377
|
|
|
350
378
|
### Credential Store
|
|
351
379
|
|
|
@@ -359,7 +387,7 @@ const models = createModels({ credentials: myFileBackedStore });
|
|
|
359
387
|
// const models = builtinModels({ credentials: myFileBackedStore });
|
|
360
388
|
```
|
|
361
389
|
|
|
362
|
-
The contract is small: `read(providerId)`, `modify(providerId, fn)` (the only write path — a serialized read-modify-write), and `delete(providerId)`. OAuth token refresh runs inside `modify`, so concurrent requests and processes cannot double-refresh a rotated token. A stored credential *owns* its provider: environment variables are only consulted when nothing is stored, and a failed refresh never silently falls back to an env key.
|
|
390
|
+
The contract is small: `read(providerId)`, `list()` for non-secret `{ providerId, type }` metadata, `modify(providerId, fn)` (the only write path — a serialized read-modify-write), and `delete(providerId)`. Enumeration must not resolve secrets or execute configured key commands. OAuth token refresh runs inside `modify`, so concurrent requests and processes cannot double-refresh a rotated token. A stored credential *owns* its provider: environment variables are only consulted when nothing is stored, and a failed refresh never silently falls back to an env key.
|
|
363
391
|
|
|
364
392
|
API-key credentials use the same discriminator as pi's `auth.json` and can carry provider-scoped env/config values:
|
|
365
393
|
|
|
@@ -412,7 +440,7 @@ Built-in providers resolve these env vars (Node.js; in browsers pass `apiKey` ex
|
|
|
412
440
|
| Xiaomi MiMo Token Plan (Singapore) | `XIAOMI_TOKEN_PLAN_SGP_API_KEY` |
|
|
413
441
|
| GitHub Copilot | `COPILOT_GITHUB_TOKEN` |
|
|
414
442
|
|
|
415
|
-
Amazon Bedrock resolves ambient AWS credentials (`AWS_PROFILE`, access key pairs, `AWS_BEARER_TOKEN_BEDROCK`, ECS task roles, web identity tokens). Vertex AI resolves either an explicit key or gcloud Application Default Credentials plus project/location.
|
|
443
|
+
Amazon Bedrock resolves ambient AWS credentials (`AWS_PROFILE`, access key pairs, `AWS_BEARER_TOKEN_BEDROCK`, ECS task roles, web identity tokens); its provider-owned login flow supports bearer tokens, AWS profiles, and the existing credential chain. Vertex AI resolves either an explicit key or gcloud Application Default Credentials plus project/location, with a provider-owned login flow for API keys, ADC, and service-account files.
|
|
416
444
|
|
|
417
445
|
## Tools
|
|
418
446
|
|
|
@@ -660,7 +688,7 @@ for (const block of result.output) {
|
|
|
660
688
|
}
|
|
661
689
|
```
|
|
662
690
|
|
|
663
|
-
Like the chat side, you can build the collection from parts: `createImagesModels({ credentials?, authContext? })`, the `openrouterImagesProvider()` factory from `@earendil-works/pi-ai/providers/openrouter-images`, and `createImagesProvider({ id, auth, models, refreshModels?, api })` for custom image providers (with `imagesModels.refresh(provider?)` for dynamic lists). Failures never reject — they return an `AssistantImages` with `stopReason: "error"`. The collection's `getAuth(
|
|
691
|
+
Like the chat side, you can build the collection from parts: `createImagesModels({ credentials?, authContext? })`, the `openrouterImagesProvider()` factory from `@earendil-works/pi-ai/providers/openrouter-images`, and `createImagesProvider({ id, auth, models, refreshModels?, api })` for custom image providers (with `imagesModels.refresh(provider?)` for dynamic lists). Failures never reject — they return an `AssistantImages` with `stopReason: "error"`. The collection's provider-scoped `getAuth(providerId)` works exactly like the chat-side one.
|
|
664
692
|
|
|
665
693
|
The old global API (`getImageModel()` / `getImageModels()` / `getImageProviders()` / `generateImages()`) remains available on the [compat entrypoint](#migrating-from-the-old-global-api):
|
|
666
694
|
|
|
@@ -982,22 +1010,46 @@ const gateway = createProvider({
|
|
|
982
1010
|
});
|
|
983
1011
|
```
|
|
984
1012
|
|
|
985
|
-
|
|
1013
|
+
Provider-wide endpoint or request transformations belong in the provider's API implementation: wrap the `ProviderStreams` you pass as `api` so every request goes through the transformation before dispatch. The Cloudflare providers do this to materialize account/gateway endpoint placeholders from the resolved provider env:
|
|
986
1014
|
|
|
987
1015
|
```typescript
|
|
1016
|
+
function tenantStreams(streams: ProviderStreams): ProviderStreams {
|
|
1017
|
+
const withTenant = (model: Model<Api>) => ({ ...model, baseUrl: model.baseUrl.replace('{tenant}', tenantId) });
|
|
1018
|
+
return {
|
|
1019
|
+
stream: (model, context, options) => streams.stream(withTenant(model), context, options),
|
|
1020
|
+
streamSimple: (model, context, options) => streams.streamSimple(withTenant(model), context, options),
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const tenantGateway = createProvider({
|
|
1025
|
+
id: 'tenant-gateway',
|
|
1026
|
+
auth: { apiKey: envApiKeyAuth('Gateway key', ['GATEWAY_API_KEY']) },
|
|
1027
|
+
models: [/* ... */],
|
|
1028
|
+
api: tenantStreams(openAICompletionsApi()),
|
|
1029
|
+
});
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
Dynamic model lists use `fetchModels`. `Models.refresh()` refreshes every configured dynamic provider, passing its effective API-key or refreshed OAuth credential. A `ModelsStore` persists dynamic catalogs; both stores default to in-memory implementations.
|
|
1033
|
+
|
|
1034
|
+
```typescript
|
|
1035
|
+
const models = createModels({ credentials, modelsStore });
|
|
988
1036
|
const llamacpp = createProvider({
|
|
989
1037
|
id: 'llamacpp',
|
|
990
1038
|
auth: { apiKey: { name: 'llama.cpp', resolve: async () => ({ auth: {} }) } },
|
|
991
1039
|
models: [],
|
|
992
|
-
|
|
1040
|
+
fetchModels: async ({ signal }) => fetchModelsFromServer('http://localhost:8080', signal),
|
|
993
1041
|
api: openAICompletionsApi(),
|
|
994
1042
|
});
|
|
995
1043
|
|
|
996
1044
|
models.setProvider(llamacpp);
|
|
997
|
-
await models.refresh(
|
|
1045
|
+
const result = await models.refresh({ signal });
|
|
1046
|
+
if (result.aborted) console.log('refresh cancelled');
|
|
1047
|
+
for (const [provider, error] of result.errors) console.error(provider, error);
|
|
998
1048
|
```
|
|
999
1049
|
|
|
1000
|
-
|
|
1050
|
+
Use `models.refresh({ allowNetwork: false })` to restore persisted catalogs without network access, or `models.refresh({ force: true })` to bypass provider freshness checks. Model reads stay synchronous and return the last restored or refreshed list.
|
|
1051
|
+
|
|
1052
|
+
Custom models can carry `headers` (e.g. proxies behind bot detection) and `compat` flags. `Models.getAuth(model)` includes those model headers, and stream methods merge them before explicit request headers and `transformHeaders`. See [OpenAI Compatibility Settings](#openai-compatibility-settings).
|
|
1001
1053
|
|
|
1002
1054
|
Some OpenAI-compatible servers do not understand the `developer` role used for reasoning-capable models. For those providers, set `compat.supportsDeveloperRole` to `false` so the system prompt is sent as a `system` message instead. If the server also does not support `reasoning_effort`, set `compat.supportsReasoningEffort` to `false` too. This commonly applies to Ollama, vLLM, SGLang, and similar OpenAI-compatible servers.
|
|
1003
1055
|
|
|
@@ -1070,7 +1122,8 @@ interface OpenAICompletionsCompat {
|
|
|
1070
1122
|
supportsReasoningEffort?: boolean; // Whether provider supports `reasoning_effort` (default: true)
|
|
1071
1123
|
supportsUsageInStreaming?: boolean; // Whether provider supports `stream_options: { include_usage: true }` (default: true)
|
|
1072
1124
|
supportsStrictMode?: boolean; // Whether provider supports `strict` in tool definitions (default: true)
|
|
1073
|
-
sendSessionAffinityHeaders?: boolean; //
|
|
1125
|
+
sendSessionAffinityHeaders?: boolean; // Send session-affinity data from `sessionId` (default: false)
|
|
1126
|
+
sessionAffinityFormat?: 'openai' | 'openai-nosession' | 'openrouter'; // Format for session affinity: 'openai' uses `prompt_cache_key`, `session_id`, `x-client-request-id`, and `x-session-affinity`; 'openai-nosession' uses `prompt_cache_key`, `x-client-request-id`, and `x-session-affinity`; 'openrouter' uses `x-session-id` (default: auto-detected)
|
|
1074
1127
|
maxTokensField?: 'max_completion_tokens' | 'max_tokens'; // Which field name to use (default: max_completion_tokens)
|
|
1075
1128
|
requiresToolResultName?: boolean; // Whether tool results require the `name` field (default: false)
|
|
1076
1129
|
requiresAssistantAfterToolResult?: boolean; // Whether tool results must be followed by an assistant message (default: false)
|
|
@@ -1085,7 +1138,7 @@ interface OpenAICompletionsCompat {
|
|
|
1085
1138
|
|
|
1086
1139
|
interface OpenAIResponsesCompat {
|
|
1087
1140
|
supportsDeveloperRole?: boolean; // Whether provider supports `developer` role vs `system` (default: true)
|
|
1088
|
-
|
|
1141
|
+
sessionAffinityFormat?: 'openai' | 'openai-nosession' | 'openrouter'; // Session-affinity header format: 'openai' sends `session_id` and `x-client-request-id`; 'openai-nosession' sends `x-client-request-id`; 'openrouter' sends `x-session-id`. Does not affect the `prompt_cache_key` body param (default: auto-detected)
|
|
1089
1142
|
supportsLongCacheRetention?: boolean; // Whether provider supports `prompt_cache_retention: "24h"` (default: true)
|
|
1090
1143
|
}
|
|
1091
1144
|
```
|
|
@@ -1368,7 +1421,7 @@ Several providers support OAuth authentication instead of static API keys:
|
|
|
1368
1421
|
- **OpenAI Codex** (ChatGPT Plus/Pro subscription, access to GPT-5.x Codex models)
|
|
1369
1422
|
- **GitHub Copilot** (Copilot subscription)
|
|
1370
1423
|
|
|
1371
|
-
Each of these providers carries an `OAuthAuth` on `provider.auth.oauth` with three operations: `login(
|
|
1424
|
+
Each of these providers carries an `OAuthAuth` on `provider.auth.oauth` with three operations: `login(interaction)` uses the provider-neutral `AuthInteraction.prompt()`/`notify()` protocol and returns a credential, `refresh(credential)` exchanges the refresh token, and `toAuth(credential)` derives request auth (GitHub Copilot's per-account base URL comes from here). Refresh is automatic: `models.getAuth(providerId)` and request paths refresh expired tokens under a credential-store lock, so concurrent requests and processes cannot double-refresh.
|
|
1372
1425
|
|
|
1373
1426
|
```typescript
|
|
1374
1427
|
import { createModels } from '@earendil-works/pi-ai';
|
|
@@ -1377,34 +1430,36 @@ import { anthropicProvider } from '@earendil-works/pi-ai/providers/anthropic';
|
|
|
1377
1430
|
const models = createModels({ credentials: myStore }); // persistent CredentialStore
|
|
1378
1431
|
models.setProvider(anthropicProvider());
|
|
1379
1432
|
|
|
1380
|
-
// Login:
|
|
1381
|
-
|
|
1382
|
-
const credential = await provider.auth.oauth!.login({
|
|
1433
|
+
// Login: Models drives the flow and persists the credential
|
|
1434
|
+
await models.login('anthropic', 'oauth', {
|
|
1383
1435
|
prompt: async (p) => {
|
|
1384
1436
|
// p.type: 'text' | 'secret' | 'select' | 'manual_code'
|
|
1385
1437
|
// manual_code prompts race a local callback server; p.signal aborts them when the server wins
|
|
1386
1438
|
return await askUser(p.message);
|
|
1387
1439
|
},
|
|
1388
1440
|
notify: (event) => {
|
|
1389
|
-
// event.type: 'auth_url' | 'device_code' | 'progress'
|
|
1441
|
+
// event.type: 'info' | 'auth_url' | 'device_code' | 'progress'
|
|
1442
|
+
if (event.type === 'info') {
|
|
1443
|
+
console.log(event.message);
|
|
1444
|
+
for (const link of event.links ?? []) console.log(`${link.label ?? 'More information'}: ${link.url}`);
|
|
1445
|
+
}
|
|
1390
1446
|
if (event.type === 'auth_url') console.log(`Open: ${event.url}`);
|
|
1391
1447
|
if (event.type === 'device_code') console.log(`Code: ${event.userCode} at ${event.verificationUri}`);
|
|
1392
1448
|
if (event.type === 'progress') console.log(event.message);
|
|
1393
1449
|
},
|
|
1394
1450
|
});
|
|
1395
|
-
await myStore.modify('anthropic', async () => credential);
|
|
1396
1451
|
|
|
1397
1452
|
// From here on, requests resolve and refresh the token automatically
|
|
1398
1453
|
const model = models.getModel('anthropic', 'claude-sonnet-4-5')!;
|
|
1399
1454
|
await models.complete(model, context);
|
|
1400
1455
|
|
|
1401
1456
|
// Logout
|
|
1402
|
-
await
|
|
1457
|
+
await models.logout('anthropic');
|
|
1403
1458
|
```
|
|
1404
1459
|
|
|
1405
1460
|
### Vertex AI
|
|
1406
1461
|
|
|
1407
|
-
Vertex AI models support either a Google Cloud API key or Application Default Credentials (ADC):
|
|
1462
|
+
Vertex AI models support either a Google Cloud API key or Application Default Credentials (ADC). Its provider-owned API-key login flow can configure either method:
|
|
1408
1463
|
|
|
1409
1464
|
- **API key**: Set `GOOGLE_CLOUD_API_KEY` or pass `apiKey` in the call options.
|
|
1410
1465
|
- **Local development (ADC)**: Run `gcloud auth application-default login`
|
|
@@ -1438,7 +1493,7 @@ Credentials are saved to `auth.json` in the current directory.
|
|
|
1438
1493
|
|
|
1439
1494
|
### Programmatic OAuth
|
|
1440
1495
|
|
|
1441
|
-
|
|
1496
|
+
Built-in login and refresh flows are private provider implementations. Use provider-owned `OAuthAuth`, which composes with `CredentialStore` and gets locked auto-refresh through `Models`. The `@earendil-works/pi-ai/oauth` entry point retains only type declarations required by coding-agent extension OAuth compatibility.
|
|
1442
1497
|
|
|
1443
1498
|
Provider notes:
|
|
1444
1499
|
|
|
@@ -1468,7 +1523,7 @@ Compat is a strict superset of the root entrypoint, so a file can switch its imp
|
|
|
1468
1523
|
| `getModels('anthropic')` / `getProviders()` | `models.getModels('anthropic')` / `models.getProviders()` or `getBuiltin*` |
|
|
1469
1524
|
| `stream(model, ctx, opts)` (env-key injection) | `models.stream(model, ctx, opts)` (provider auth resolution) |
|
|
1470
1525
|
| `registerApiProvider({ api, stream, streamSimple })` | `createProvider({ id, auth, models, api })` + `models.setProvider()` |
|
|
1471
|
-
| `getEnvApiKey('openai')` | `await models.getAuth(model)` |
|
|
1526
|
+
| `getEnvApiKey('openai')` | `await models.getAuth(model.provider)` |
|
|
1472
1527
|
| `streamAnthropic(model, ctx, opts)` | `stream` from `@earendil-works/pi-ai/api/anthropic-messages`, or a provider in a collection |
|
|
1473
1528
|
| `registerFauxProvider()` | `fauxProvider()` + `models.setProvider()` |
|
|
1474
1529
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"anthropic-messages.d.ts","sourceRoot":"","sources":["../../src/api/anthropic-messages.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAU1C,OAAO,KAAK,EAWX,mBAAmB,EAEnB,cAAc,EACd,aAAa,EAMb,MAAM,aAAa,CAAC;AAoIrB,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;AAE1E,MAAM,MAAM,wBAAwB,GAAG,YAAY,GAAG,SAAS,CAAC;AAkBhE,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACtD;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,wBAAwB,CAAC;IAC3C;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;CACnB;AAiOD,eAAO,MAAM,MAAM,EAAE,cAAc,CAAC,oBAAoB,EAAE,gBAAgB,CAiRzE,CAAC;AA2BF,eAAO,MAAM,YAAY,EAAE,cAAc,CAAC,oBAAoB,EAAE,mBAAmB,CAwClF,CAAC","sourcesContent":["import Anthropic from \"@anthropic-ai/sdk\";\nimport type {\n\tCacheControlEphemeral,\n\tContentBlockParam,\n\tMessageCreateParamsStreaming,\n\tMessageParam,\n\tRawMessageStreamEvent,\n\tRefusalStopDetails,\n} from \"@anthropic-ai/sdk/resources/messages.js\";\nimport { calculateCost } from \"../models.ts\";\nimport type {\n\tAnthropicMessagesCompat,\n\tApi,\n\tAssistantMessage,\n\tCacheRetention,\n\tContext,\n\tImageContent,\n\tMessage,\n\tModel,\n\tProviderEnv,\n\tProviderHeaders,\n\tSimpleStreamOptions,\n\tStopReason,\n\tStreamFunction,\n\tStreamOptions,\n\tTextContent,\n\tThinkingContent,\n\tTool,\n\tToolCall,\n\tToolResultMessage,\n} from \"../types.ts\";\nimport { AssistantMessageEventStream } from \"../utils/event-stream.ts\";\nimport { headersToRecord } from \"../utils/headers.ts\";\nimport { parseJsonWithRepair, parseStreamingJson } from \"../utils/json-parse.ts\";\nimport { getProviderEnvValue } from \"../utils/provider-env.ts\";\nimport { sanitizeSurrogates } from \"../utils/sanitize-unicode.ts\";\n\nimport { buildCopilotDynamicHeaders, hasCopilotVisionInput } from \"./github-copilot-headers.ts\";\nimport { adjustMaxTokensForThinking, buildBaseOptions, clampMaxTokensToContext } from \"./simple-options.ts\";\nimport { transformMessages } from \"./transform-messages.ts\";\n\n/**\n * Resolve cache retention preference.\n * Defaults to \"short\" and uses PI_CACHE_RETENTION for backward compatibility.\n */\nfunction resolveCacheRetention(cacheRetention?: CacheRetention, env?: ProviderEnv): CacheRetention {\n\tif (cacheRetention) {\n\t\treturn cacheRetention;\n\t}\n\tif (getProviderEnvValue(\"PI_CACHE_RETENTION\", env) === \"long\") {\n\t\treturn \"long\";\n\t}\n\treturn \"short\";\n}\n\nfunction getCacheControl(\n\tmodel: Model<\"anthropic-messages\">,\n\tcacheRetention?: CacheRetention,\n\tenv?: ProviderEnv,\n): { retention: CacheRetention; cacheControl?: CacheControlEphemeral } {\n\tconst retention = resolveCacheRetention(cacheRetention, env);\n\tif (retention === \"none\") {\n\t\treturn { retention };\n\t}\n\tconst ttl = retention === \"long\" && getAnthropicCompat(model).supportsLongCacheRetention ? \"1h\" : undefined;\n\treturn {\n\t\tretention,\n\t\tcacheControl: { type: \"ephemeral\", ...(ttl && { ttl }) },\n\t};\n}\n\n// Stealth mode: Mimic Claude Code's tool naming exactly\nconst claudeCodeVersion = \"2.1.75\";\n\n// Claude Code 2.x tool names (canonical casing)\n// Source: https://cchistory.mariozechner.at/data/prompts-2.1.11.md\n// To update: https://github.com/badlogic/cchistory\nconst claudeCodeTools = [\n\t\"Read\",\n\t\"Write\",\n\t\"Edit\",\n\t\"Bash\",\n\t\"Grep\",\n\t\"Glob\",\n\t\"AskUserQuestion\",\n\t\"EnterPlanMode\",\n\t\"ExitPlanMode\",\n\t\"KillShell\",\n\t\"NotebookEdit\",\n\t\"Skill\",\n\t\"Task\",\n\t\"TaskOutput\",\n\t\"TodoWrite\",\n\t\"WebFetch\",\n\t\"WebSearch\",\n];\n\nconst ccToolLookup = new Map(claudeCodeTools.map((t) => [t.toLowerCase(), t]));\n\n// Convert tool name to CC canonical casing if it matches (case-insensitive)\nconst toClaudeCodeName = (name: string) => ccToolLookup.get(name.toLowerCase()) ?? name;\nconst fromClaudeCodeName = (name: string, tools?: Tool[]) => {\n\tif (tools && tools.length > 0) {\n\t\tconst lowerName = name.toLowerCase();\n\t\tconst matchedTool = tools.find((tool) => tool.name.toLowerCase() === lowerName);\n\t\tif (matchedTool) return matchedTool.name;\n\t}\n\treturn name;\n};\n\n/**\n * Convert content blocks to Anthropic API format\n */\nfunction convertContentBlocks(content: (TextContent | ImageContent)[]):\n\t| string\n\t| Array<\n\t\t\t| { type: \"text\"; text: string }\n\t\t\t| {\n\t\t\t\t\ttype: \"image\";\n\t\t\t\t\tsource: {\n\t\t\t\t\t\ttype: \"base64\";\n\t\t\t\t\t\tmedia_type: \"image/jpeg\" | \"image/png\" | \"image/gif\" | \"image/webp\";\n\t\t\t\t\t\tdata: string;\n\t\t\t\t\t};\n\t\t\t }\n\t > {\n\t// If only text blocks, return as concatenated string for simplicity\n\tconst hasImages = content.some((c) => c.type === \"image\");\n\tif (!hasImages) {\n\t\treturn sanitizeSurrogates(content.map((c) => (c as TextContent).text).join(\"\\n\"));\n\t}\n\n\t// If we have images, convert to content block array\n\tconst blocks = content.map((block) => {\n\t\tif (block.type === \"text\") {\n\t\t\treturn {\n\t\t\t\ttype: \"text\" as const,\n\t\t\t\ttext: sanitizeSurrogates(block.text),\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\ttype: \"image\" as const,\n\t\t\tsource: {\n\t\t\t\ttype: \"base64\" as const,\n\t\t\t\tmedia_type: block.mimeType as \"image/jpeg\" | \"image/png\" | \"image/gif\" | \"image/webp\",\n\t\t\t\tdata: block.data,\n\t\t\t},\n\t\t};\n\t});\n\n\t// If only images (no text), add placeholder text block\n\tconst hasText = blocks.some((b) => b.type === \"text\");\n\tif (!hasText) {\n\t\tblocks.unshift({\n\t\t\ttype: \"text\" as const,\n\t\t\ttext: \"(see attached image)\",\n\t\t});\n\t}\n\n\treturn blocks;\n}\n\nexport type AnthropicEffort = \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\";\n\nexport type AnthropicThinkingDisplay = \"summarized\" | \"omitted\";\n\nconst FINE_GRAINED_TOOL_STREAMING_BETA = \"fine-grained-tool-streaming-2025-05-14\";\nconst INTERLEAVED_THINKING_BETA = \"interleaved-thinking-2025-05-14\";\n\nfunction getAnthropicCompat(\n\tmodel: Model<\"anthropic-messages\">,\n): Required<Omit<AnthropicMessagesCompat, \"forceAdaptiveThinking\">> {\n\treturn {\n\t\tsupportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? true,\n\t\tsupportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? true,\n\t\tsendSessionAffinityHeaders: model.compat?.sendSessionAffinityHeaders ?? false,\n\t\tsupportsCacheControlOnTools: model.compat?.supportsCacheControlOnTools ?? true,\n\t\tsupportsTemperature: model.compat?.supportsTemperature ?? true,\n\t\tallowEmptySignature: model.compat?.allowEmptySignature ?? false,\n\t};\n}\n\nexport interface AnthropicOptions extends StreamOptions {\n\t/**\n\t * Enable extended thinking.\n\t * For adaptive thinking models: the model decides when/how much to think.\n\t * For older models: uses budget-based thinking with thinkingBudgetTokens.\n\t * Default: undefined (thinking is omitted unless `streamSimple()` maps\n\t * a simple reasoning level to this option, or callers set it explicitly).\n\t */\n\tthinkingEnabled?: boolean;\n\t/**\n\t * Token budget for extended thinking (older models only).\n\t * Ignored for adaptive thinking models.\n\t * Default: 1024 when `thinkingEnabled` is true and no budget is provided.\n\t */\n\tthinkingBudgetTokens?: number;\n\t/**\n\t * Effort level for adaptive thinking models.\n\t * Controls how much thinking Claude allocates:\n\t * - \"max\": Always thinks with no constraints (Opus 4.6 only)\n\t * - \"xhigh\": Highest reasoning level (Opus 4.7+, Fable 5)\n\t * - \"high\": Always thinks, deep reasoning\n\t * - \"medium\": Moderate thinking, may skip for simple queries\n\t * - \"low\": Minimal thinking, skips for simple tasks\n\t * Ignored for older models.\n\t * Default: omitted unless `streamSimple()` maps a simple reasoning\n\t * level to this option.\n\t */\n\teffort?: AnthropicEffort;\n\t/**\n\t * Controls how thinking content is returned in API responses.\n\t * - \"summarized\": Thinking blocks contain summarized thinking text.\n\t * - \"omitted\": Thinking blocks return an empty thinking field; the encrypted\n\t * signature still travels back for multi-turn continuity. Use for faster\n\t * time-to-first-text-token when your UI does not surface thinking.\n\t *\n\t * Note: Anthropic's API default for Claude Opus 4.7 and Claude Mythos Preview\n\t * is \"omitted\". We default to \"summarized\" here to keep behavior consistent\n\t * with older Claude 4 models. Set this explicitly to \"omitted\" to opt in.\n\t * Default: \"summarized\" when thinking is enabled.\n\t */\n\tthinkingDisplay?: AnthropicThinkingDisplay;\n\t/**\n\t * Whether to request the interleaved thinking beta header for non-adaptive\n\t * thinking models. Adaptive thinking models have interleaved thinking built in,\n\t * so the header is skipped for them regardless of this setting.\n\t * Default: true.\n\t */\n\tinterleavedThinking?: boolean;\n\t/**\n\t * Anthropic tool choice behavior. String values map to Anthropic's built-in\n\t * choices; `{ type: \"tool\", name }` forces a specific tool.\n\t * Default: omitted (Anthropic default behavior, currently equivalent to auto).\n\t */\n\ttoolChoice?: \"auto\" | \"any\" | \"none\" | { type: \"tool\"; name: string };\n\t/**\n\t * Pre-built Anthropic client instance. When provided, skips internal client\n\t * construction entirely. Use this to inject alternative SDK clients such as\n\t * `AnthropicVertex` that shares the same messaging API.\n\t */\n\tclient?: Anthropic;\n}\n\nfunction mergeHeaders(...headerSources: (ProviderHeaders | undefined)[]): ProviderHeaders {\n\tconst merged: ProviderHeaders = {};\n\tfor (const headers of headerSources) {\n\t\tif (headers) {\n\t\t\tObject.assign(merged, headers);\n\t\t}\n\t}\n\treturn merged;\n}\n\nfunction hasHeader(headers: ProviderHeaders | undefined, name: string): boolean {\n\tif (!headers) return false;\n\tconst expected = name.toLowerCase();\n\tfor (const [key, value] of Object.entries(headers)) {\n\t\tif (key.toLowerCase() === expected && value !== null && value.trim().length > 0) return true;\n\t}\n\treturn false;\n}\n\nfunction assertRequestAuth(provider: string, apiKey: string | undefined, headers: ProviderHeaders | undefined): void {\n\tif (apiKey) return;\n\tif (\n\t\thasHeader(headers, \"authorization\") ||\n\t\thasHeader(headers, \"x-api-key\") ||\n\t\thasHeader(headers, \"cf-aig-authorization\")\n\t) {\n\t\treturn;\n\t}\n\tthrow new Error(`No API key for provider: ${provider}`);\n}\n\ninterface ServerSentEvent {\n\tevent: string | null;\n\tdata: string;\n\traw: string[];\n}\n\ninterface SseDecoderState {\n\tevent: string | null;\n\tdata: string[];\n\traw: string[];\n}\n\nconst ANTHROPIC_MESSAGE_EVENTS: ReadonlySet<string> = new Set([\n\t\"message_start\",\n\t\"message_delta\",\n\t\"message_stop\",\n\t\"content_block_start\",\n\t\"content_block_delta\",\n\t\"content_block_stop\",\n]);\n\nfunction flushSseEvent(state: SseDecoderState): ServerSentEvent | null {\n\tif (!state.event && state.data.length === 0) {\n\t\treturn null;\n\t}\n\n\tconst event: ServerSentEvent = {\n\t\tevent: state.event,\n\t\tdata: state.data.join(\"\\n\"),\n\t\traw: [...state.raw],\n\t};\n\tstate.event = null;\n\tstate.data = [];\n\tstate.raw = [];\n\treturn event;\n}\n\nfunction decodeSseLine(line: string, state: SseDecoderState): ServerSentEvent | null {\n\tif (line === \"\") {\n\t\treturn flushSseEvent(state);\n\t}\n\n\tstate.raw.push(line);\n\tif (line.startsWith(\":\")) {\n\t\treturn null;\n\t}\n\n\tconst delimiterIndex = line.indexOf(\":\");\n\tconst fieldName = delimiterIndex === -1 ? line : line.slice(0, delimiterIndex);\n\tlet value = delimiterIndex === -1 ? \"\" : line.slice(delimiterIndex + 1);\n\tif (value.startsWith(\" \")) {\n\t\tvalue = value.slice(1);\n\t}\n\n\tif (fieldName === \"event\") {\n\t\tstate.event = value;\n\t} else if (fieldName === \"data\") {\n\t\tstate.data.push(value);\n\t}\n\n\treturn null;\n}\n\nfunction nextLineBreakIndex(text: string): number {\n\tconst carriageReturnIndex = text.indexOf(\"\\r\");\n\tconst newlineIndex = text.indexOf(\"\\n\");\n\tif (carriageReturnIndex === -1) {\n\t\treturn newlineIndex;\n\t}\n\tif (newlineIndex === -1) {\n\t\treturn carriageReturnIndex;\n\t}\n\treturn Math.min(carriageReturnIndex, newlineIndex);\n}\n\nfunction consumeLine(text: string): { line: string; rest: string } | null {\n\tconst lineBreakIndex = nextLineBreakIndex(text);\n\tif (lineBreakIndex === -1) {\n\t\treturn null;\n\t}\n\n\tlet nextIndex = lineBreakIndex + 1;\n\tif (text[lineBreakIndex] === \"\\r\" && text[nextIndex] === \"\\n\") {\n\t\tnextIndex += 1;\n\t}\n\n\treturn {\n\t\tline: text.slice(0, lineBreakIndex),\n\t\trest: text.slice(nextIndex),\n\t};\n}\n\nasync function* iterateSseMessages(\n\tbody: ReadableStream<Uint8Array>,\n\tsignal?: AbortSignal,\n): AsyncGenerator<ServerSentEvent> {\n\tconst reader = body.getReader();\n\tconst decoder = new TextDecoder();\n\tconst state: SseDecoderState = { event: null, data: [], raw: [] };\n\tlet buffer = \"\";\n\n\ttry {\n\t\twhile (true) {\n\t\t\tif (signal?.aborted) {\n\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t}\n\n\t\t\tconst { value, done } = await reader.read();\n\t\t\tif (done) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tbuffer += decoder.decode(value, { stream: true });\n\t\t\tlet consumed = consumeLine(buffer);\n\t\t\twhile (consumed) {\n\t\t\t\tbuffer = consumed.rest;\n\t\t\t\tconst event = decodeSseLine(consumed.line, state);\n\t\t\t\tif (event) {\n\t\t\t\t\tyield event;\n\t\t\t\t}\n\t\t\t\tconsumed = consumeLine(buffer);\n\t\t\t}\n\t\t}\n\n\t\tbuffer += decoder.decode();\n\t\tlet consumed = consumeLine(buffer);\n\t\twhile (consumed) {\n\t\t\tbuffer = consumed.rest;\n\t\t\tconst event = decodeSseLine(consumed.line, state);\n\t\t\tif (event) {\n\t\t\t\tyield event;\n\t\t\t}\n\t\t\tconsumed = consumeLine(buffer);\n\t\t}\n\n\t\tif (buffer.length > 0) {\n\t\t\tconst event = decodeSseLine(buffer, state);\n\t\t\tif (event) {\n\t\t\t\tyield event;\n\t\t\t}\n\t\t}\n\n\t\tconst trailingEvent = flushSseEvent(state);\n\t\tif (trailingEvent) {\n\t\t\tyield trailingEvent;\n\t\t}\n\t} finally {\n\t\treader.releaseLock();\n\t}\n}\n\nasync function* iterateAnthropicEvents(\n\tresponse: Response,\n\tsignal?: AbortSignal,\n): AsyncGenerator<RawMessageStreamEvent> {\n\tif (!response.body) {\n\t\tthrow new Error(\"Attempted to iterate over an Anthropic response with no body\");\n\t}\n\n\tlet sawMessageStart = false;\n\tlet sawMessageEnd = false;\n\n\tfor await (const sse of iterateSseMessages(response.body, signal)) {\n\t\tif (sse.event === \"error\") {\n\t\t\tthrow new Error(sse.data);\n\t\t}\n\n\t\tif (!ANTHROPIC_MESSAGE_EVENTS.has(sse.event ?? \"\")) {\n\t\t\tcontinue;\n\t\t}\n\n\t\ttry {\n\t\t\tconst event = parseJsonWithRepair<RawMessageStreamEvent>(sse.data);\n\t\t\tif (event.type === \"message_start\") {\n\t\t\t\tsawMessageStart = true;\n\t\t\t} else if (event.type === \"message_stop\") {\n\t\t\t\tsawMessageEnd = true;\n\t\t\t}\n\t\t\tyield event;\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tthrow new Error(\n\t\t\t\t`Could not parse Anthropic SSE event ${sse.event}: ${message}; data=${sse.data}; raw=${sse.raw.join(\"\\\\n\")}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tif (sawMessageStart && !sawMessageEnd) {\n\t\tthrow new Error(\"Anthropic stream ended before message_stop\");\n\t}\n}\n\nexport const stream: StreamFunction<\"anthropic-messages\", AnthropicOptions> = (\n\tmodel: Model<\"anthropic-messages\">,\n\tcontext: Context,\n\toptions?: AnthropicOptions,\n): AssistantMessageEventStream => {\n\tconst stream = new AssistantMessageEventStream();\n\n\t(async () => {\n\t\tconst output: AssistantMessage = {\n\t\t\trole: \"assistant\",\n\t\t\tcontent: [],\n\t\t\tapi: model.api as Api,\n\t\t\tprovider: model.provider,\n\t\t\tmodel: model.id,\n\t\t\tusage: {\n\t\t\t\tinput: 0,\n\t\t\t\toutput: 0,\n\t\t\t\tcacheRead: 0,\n\t\t\t\tcacheWrite: 0,\n\t\t\t\ttotalTokens: 0,\n\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t\t},\n\t\t\tstopReason: \"stop\",\n\t\t\ttimestamp: Date.now(),\n\t\t};\n\n\t\ttry {\n\t\t\tlet client: Anthropic;\n\t\t\tlet isOAuth: boolean;\n\n\t\t\tif (options?.client) {\n\t\t\t\tclient = options.client;\n\t\t\t\tisOAuth = false;\n\t\t\t} else {\n\t\t\t\tconst apiKey = options?.apiKey;\n\t\t\t\tassertRequestAuth(model.provider, apiKey, options?.headers);\n\n\t\t\t\tlet copilotDynamicHeaders: Record<string, string> | undefined;\n\t\t\t\tif (model.provider === \"github-copilot\") {\n\t\t\t\t\tconst hasImages = hasCopilotVisionInput(context.messages);\n\t\t\t\t\tcopilotDynamicHeaders = buildCopilotDynamicHeaders({\n\t\t\t\t\t\tmessages: context.messages,\n\t\t\t\t\t\thasImages,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tconst cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env);\n\t\t\t\tconst cacheSessionId = cacheRetention === \"none\" ? undefined : options?.sessionId;\n\n\t\t\t\tconst created = createClient(\n\t\t\t\t\tmodel,\n\t\t\t\t\tapiKey,\n\t\t\t\t\toptions?.interleavedThinking ?? true,\n\t\t\t\t\tshouldUseFineGrainedToolStreamingBeta(model, context),\n\t\t\t\t\toptions?.headers,\n\t\t\t\t\tcopilotDynamicHeaders,\n\t\t\t\t\tcacheSessionId,\n\t\t\t\t);\n\t\t\t\tclient = created.client;\n\t\t\t\tisOAuth = created.isOAuthToken;\n\t\t\t}\n\t\t\tlet params = buildParams(model, context, isOAuth, options);\n\t\t\tconst nextParams = await options?.onPayload?.(params, model);\n\t\t\tif (nextParams !== undefined) {\n\t\t\t\tparams = nextParams as MessageCreateParamsStreaming;\n\t\t\t}\n\t\t\tconst requestOptions = {\n\t\t\t\t...(options?.signal ? { signal: options.signal } : {}),\n\t\t\t\t...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),\n\t\t\t\tmaxRetries: options?.maxRetries ?? 0,\n\t\t\t};\n\t\t\tconst response = await client.messages.create({ ...params, stream: true }, requestOptions).asResponse();\n\t\t\tawait options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);\n\t\t\tstream.push({ type: \"start\", partial: output });\n\n\t\t\ttype Block = (ThinkingContent | TextContent | (ToolCall & { partialJson: string })) & { index: number };\n\t\t\tconst blocks = output.content as Block[];\n\n\t\t\tfor await (const event of iterateAnthropicEvents(response, options?.signal)) {\n\t\t\t\tif (event.type === \"message_start\") {\n\t\t\t\t\toutput.responseId = event.message.id;\n\t\t\t\t\t// Capture initial token usage from message_start event\n\t\t\t\t\t// This ensures we have input token counts even if the stream is aborted early\n\t\t\t\t\toutput.usage.input = event.message.usage.input_tokens || 0;\n\t\t\t\t\toutput.usage.output = event.message.usage.output_tokens || 0;\n\t\t\t\t\toutput.usage.cacheRead = event.message.usage.cache_read_input_tokens || 0;\n\t\t\t\t\toutput.usage.cacheWrite = event.message.usage.cache_creation_input_tokens || 0;\n\t\t\t\t\toutput.usage.cacheWrite1h = event.message.usage.cache_creation?.ephemeral_1h_input_tokens || 0;\n\t\t\t\t\t// Anthropic doesn't provide total_tokens, compute from components\n\t\t\t\t\toutput.usage.totalTokens =\n\t\t\t\t\t\toutput.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;\n\t\t\t\t\tcalculateCost(model, output.usage);\n\t\t\t\t} else if (event.type === \"content_block_start\") {\n\t\t\t\t\tif (event.content_block.type === \"text\") {\n\t\t\t\t\t\tconst block: Block = {\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"\",\n\t\t\t\t\t\t\tindex: event.index,\n\t\t\t\t\t\t};\n\t\t\t\t\t\toutput.content.push(block);\n\t\t\t\t\t\tstream.push({ type: \"text_start\", contentIndex: output.content.length - 1, partial: output });\n\t\t\t\t\t} else if (event.content_block.type === \"thinking\") {\n\t\t\t\t\t\tconst block: Block = {\n\t\t\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\t\t\tthinking: \"\",\n\t\t\t\t\t\t\tthinkingSignature: \"\",\n\t\t\t\t\t\t\tindex: event.index,\n\t\t\t\t\t\t};\n\t\t\t\t\t\toutput.content.push(block);\n\t\t\t\t\t\tstream.push({ type: \"thinking_start\", contentIndex: output.content.length - 1, partial: output });\n\t\t\t\t\t} else if (event.content_block.type === \"redacted_thinking\") {\n\t\t\t\t\t\tconst block: Block = {\n\t\t\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\t\t\tthinking: \"[Reasoning redacted]\",\n\t\t\t\t\t\t\tthinkingSignature: event.content_block.data,\n\t\t\t\t\t\t\tredacted: true,\n\t\t\t\t\t\t\tindex: event.index,\n\t\t\t\t\t\t};\n\t\t\t\t\t\toutput.content.push(block);\n\t\t\t\t\t\tstream.push({ type: \"thinking_start\", contentIndex: output.content.length - 1, partial: output });\n\t\t\t\t\t} else if (event.content_block.type === \"tool_use\") {\n\t\t\t\t\t\tconst block: Block = {\n\t\t\t\t\t\t\ttype: \"toolCall\",\n\t\t\t\t\t\t\tid: event.content_block.id,\n\t\t\t\t\t\t\tname: isOAuth\n\t\t\t\t\t\t\t\t? fromClaudeCodeName(event.content_block.name, context.tools)\n\t\t\t\t\t\t\t\t: event.content_block.name,\n\t\t\t\t\t\t\targuments: (event.content_block.input as Record<string, any>) ?? {},\n\t\t\t\t\t\t\tpartialJson: \"\",\n\t\t\t\t\t\t\tindex: event.index,\n\t\t\t\t\t\t};\n\t\t\t\t\t\toutput.content.push(block);\n\t\t\t\t\t\tstream.push({ type: \"toolcall_start\", contentIndex: output.content.length - 1, partial: output });\n\t\t\t\t\t}\n\t\t\t\t} else if (event.type === \"content_block_delta\") {\n\t\t\t\t\tif (event.delta.type === \"text_delta\") {\n\t\t\t\t\t\tconst index = blocks.findIndex((b) => b.index === event.index);\n\t\t\t\t\t\tconst block = blocks[index];\n\t\t\t\t\t\tif (block && block.type === \"text\") {\n\t\t\t\t\t\t\tblock.text += event.delta.text;\n\t\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\t\ttype: \"text_delta\",\n\t\t\t\t\t\t\t\tcontentIndex: index,\n\t\t\t\t\t\t\t\tdelta: event.delta.text,\n\t\t\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (event.delta.type === \"thinking_delta\") {\n\t\t\t\t\t\tconst index = blocks.findIndex((b) => b.index === event.index);\n\t\t\t\t\t\tconst block = blocks[index];\n\t\t\t\t\t\tif (block && block.type === \"thinking\") {\n\t\t\t\t\t\t\tblock.thinking += event.delta.thinking;\n\t\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\t\ttype: \"thinking_delta\",\n\t\t\t\t\t\t\t\tcontentIndex: index,\n\t\t\t\t\t\t\t\tdelta: event.delta.thinking,\n\t\t\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (event.delta.type === \"input_json_delta\") {\n\t\t\t\t\t\tconst index = blocks.findIndex((b) => b.index === event.index);\n\t\t\t\t\t\tconst block = blocks[index];\n\t\t\t\t\t\tif (block && block.type === \"toolCall\") {\n\t\t\t\t\t\t\tblock.partialJson += event.delta.partial_json;\n\t\t\t\t\t\t\tblock.arguments = parseStreamingJson(block.partialJson);\n\t\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\t\ttype: \"toolcall_delta\",\n\t\t\t\t\t\t\t\tcontentIndex: index,\n\t\t\t\t\t\t\t\tdelta: event.delta.partial_json,\n\t\t\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (event.delta.type === \"signature_delta\") {\n\t\t\t\t\t\tconst index = blocks.findIndex((b) => b.index === event.index);\n\t\t\t\t\t\tconst block = blocks[index];\n\t\t\t\t\t\tif (block && block.type === \"thinking\") {\n\t\t\t\t\t\t\tblock.thinkingSignature = block.thinkingSignature || \"\";\n\t\t\t\t\t\t\tblock.thinkingSignature += event.delta.signature;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if (event.type === \"content_block_stop\") {\n\t\t\t\t\tconst index = blocks.findIndex((b) => b.index === event.index);\n\t\t\t\t\tconst block = blocks[index];\n\t\t\t\t\tif (block) {\n\t\t\t\t\t\tdelete (block as any).index;\n\t\t\t\t\t\tif (block.type === \"text\") {\n\t\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\t\ttype: \"text_end\",\n\t\t\t\t\t\t\t\tcontentIndex: index,\n\t\t\t\t\t\t\t\tcontent: block.text,\n\t\t\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\t\ttype: \"thinking_end\",\n\t\t\t\t\t\t\t\tcontentIndex: index,\n\t\t\t\t\t\t\t\tcontent: block.thinking,\n\t\t\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\t\t\tblock.arguments = parseStreamingJson(block.partialJson);\n\t\t\t\t\t\t\t// Finalize in-place and strip the scratch buffer so replay only\n\t\t\t\t\t\t\t// carries parsed arguments.\n\t\t\t\t\t\t\tdelete (block as { partialJson?: string }).partialJson;\n\t\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\t\ttype: \"toolcall_end\",\n\t\t\t\t\t\t\t\tcontentIndex: index,\n\t\t\t\t\t\t\t\ttoolCall: block,\n\t\t\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if (event.type === \"message_delta\") {\n\t\t\t\t\tif (event.delta.stop_reason) {\n\t\t\t\t\t\tconst stopReasonResult = mapStopReason(event.delta.stop_reason, event.delta.stop_details);\n\t\t\t\t\t\toutput.stopReason = stopReasonResult.stopReason;\n\t\t\t\t\t\tif (stopReasonResult.errorMessage) {\n\t\t\t\t\t\t\toutput.errorMessage = stopReasonResult.errorMessage;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t// Only update usage fields if present (not null).\n\t\t\t\t\t// Preserves input_tokens from message_start when proxies omit it in message_delta.\n\t\t\t\t\tif (event.usage.input_tokens != null) {\n\t\t\t\t\t\toutput.usage.input = event.usage.input_tokens;\n\t\t\t\t\t}\n\t\t\t\t\tif (event.usage.output_tokens != null) {\n\t\t\t\t\t\toutput.usage.output = event.usage.output_tokens;\n\t\t\t\t\t}\n\t\t\t\t\tif (event.usage.cache_read_input_tokens != null) {\n\t\t\t\t\t\toutput.usage.cacheRead = event.usage.cache_read_input_tokens;\n\t\t\t\t\t}\n\t\t\t\t\tif (event.usage.cache_creation_input_tokens != null) {\n\t\t\t\t\t\toutput.usage.cacheWrite = event.usage.cache_creation_input_tokens;\n\t\t\t\t\t}\n\t\t\t\t\t// Anthropic reports reasoning tokens in `output_tokens_details.thinking_tokens` on the\n\t\t\t\t\t// final message_delta usage (a subset of output_tokens). SDK 0.91.1 omits the field from\n\t\t\t\t\t// its Usage type, so read it through a narrow cast. Verified against the live API.\n\t\t\t\t\tconst thinkingTokens = (event.usage as { output_tokens_details?: { thinking_tokens?: number } })\n\t\t\t\t\t\t.output_tokens_details?.thinking_tokens;\n\t\t\t\t\tif (thinkingTokens != null) {\n\t\t\t\t\t\toutput.usage.reasoning = thinkingTokens;\n\t\t\t\t\t}\n\t\t\t\t\t// Anthropic doesn't provide total_tokens, compute from components\n\t\t\t\t\toutput.usage.totalTokens =\n\t\t\t\t\t\toutput.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;\n\t\t\t\t\tcalculateCost(model, output.usage);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (options?.signal?.aborted) {\n\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t}\n\n\t\t\tif (output.stopReason === \"aborted\" || output.stopReason === \"error\") {\n\t\t\t\tthrow new Error(output.errorMessage || \"An unknown error occurred\");\n\t\t\t}\n\n\t\t\tstream.push({ type: \"done\", reason: output.stopReason, message: output });\n\t\t\tstream.end();\n\t\t} catch (error) {\n\t\t\tfor (const block of output.content) {\n\t\t\t\tdelete (block as { index?: number }).index;\n\t\t\t\t// partialJson is only a streaming scratch buffer; never persist it.\n\t\t\t\tdelete (block as { partialJson?: string }).partialJson;\n\t\t\t}\n\t\t\toutput.stopReason = options?.signal?.aborted ? \"aborted\" : \"error\";\n\t\t\toutput.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);\n\t\t\tstream.push({ type: \"error\", reason: output.stopReason, error: output });\n\t\t\tstream.end();\n\t\t}\n\t})();\n\n\treturn stream;\n};\n\n/**\n * Map ThinkingLevel to Anthropic effort levels for adaptive thinking.\n * Note: effort \"max\" is available on all adaptive-thinking Claude models, while native\n * \"xhigh\" is only available on Opus 4.7/4.8, Sonnet 5, and Fable 5.\n */\nfunction mapThinkingLevelToEffort(\n\tmodel: Model<\"anthropic-messages\">,\n\tlevel: SimpleStreamOptions[\"reasoning\"],\n): AnthropicEffort {\n\tconst mapped = level ? model.thinkingLevelMap?.[level] : undefined;\n\tif (typeof mapped === \"string\") return mapped as AnthropicEffort;\n\n\tswitch (level) {\n\t\tcase \"minimal\":\n\t\tcase \"low\":\n\t\t\treturn \"low\";\n\t\tcase \"medium\":\n\t\t\treturn \"medium\";\n\t\tcase \"high\":\n\t\t\treturn \"high\";\n\t\tdefault:\n\t\t\treturn \"high\";\n\t}\n}\n\nexport const streamSimple: StreamFunction<\"anthropic-messages\", SimpleStreamOptions> = (\n\tmodel: Model<\"anthropic-messages\">,\n\tcontext: Context,\n\toptions?: SimpleStreamOptions,\n): AssistantMessageEventStream => {\n\tassertRequestAuth(model.provider, options?.apiKey, options?.headers);\n\n\tconst base = buildBaseOptions(model, context, options, options?.apiKey);\n\tif (!options?.reasoning) {\n\t\treturn stream(model, context, { ...base, thinkingEnabled: false } satisfies AnthropicOptions);\n\t}\n\n\t// For models with adaptive thinking: use an effort level.\n\t// For older models: use budget-based thinking.\n\tif (model.compat?.forceAdaptiveThinking === true) {\n\t\tconst effort = mapThinkingLevelToEffort(model, options.reasoning);\n\t\treturn stream(model, context, {\n\t\t\t...base,\n\t\t\tthinkingEnabled: true,\n\t\t\teffort,\n\t\t} satisfies AnthropicOptions);\n\t}\n\n\t// Undefined means the caller did not request an output cap; let the helper use the model cap.\n\t// Do not coerce to 0 here, or the thinking budget would become the entire max_tokens value.\n\tconst adjusted = adjustMaxTokensForThinking(\n\t\tbase.maxTokens,\n\t\tmodel.maxTokens,\n\t\toptions.reasoning,\n\t\toptions.thinkingBudgets,\n\t);\n\n\tconst maxTokens = clampMaxTokensToContext(model, context, adjusted.maxTokens);\n\n\treturn stream(model, context, {\n\t\t...base,\n\t\tmaxTokens,\n\t\tthinkingEnabled: true,\n\t\tthinkingBudgetTokens: Math.min(adjusted.thinkingBudget, Math.max(0, maxTokens - 1024)),\n\t} satisfies AnthropicOptions);\n};\n\nfunction isOAuthToken(apiKey: string): boolean {\n\treturn apiKey.includes(\"sk-ant-oat\");\n}\n\nfunction createClient(\n\tmodel: Model<\"anthropic-messages\">,\n\tapiKey: string | undefined,\n\tinterleavedThinking: boolean,\n\tuseFineGrainedToolStreamingBeta: boolean,\n\toptionsHeaders?: ProviderHeaders,\n\tdynamicHeaders?: Record<string, string>,\n\tsessionId?: string,\n): { client: Anthropic; isOAuthToken: boolean } {\n\t// Adaptive thinking models have interleaved thinking built in, so skip the beta header.\n\tconst needsInterleavedBeta = interleavedThinking && model.compat?.forceAdaptiveThinking !== true;\n\tconst betaFeatures: string[] = [];\n\tif (useFineGrainedToolStreamingBeta) {\n\t\tbetaFeatures.push(FINE_GRAINED_TOOL_STREAMING_BETA);\n\t}\n\tif (needsInterleavedBeta) {\n\t\tbetaFeatures.push(INTERLEAVED_THINKING_BETA);\n\t}\n\n\t// Copilot: Bearer auth, selective betas.\n\tif (model.provider === \"github-copilot\") {\n\t\tconst client = new Anthropic({\n\t\t\tapiKey: null,\n\t\t\tauthToken: apiKey ?? null,\n\t\t\tbaseURL: model.baseUrl,\n\t\t\tdangerouslyAllowBrowser: true,\n\t\t\tdefaultHeaders: mergeHeaders(\n\t\t\t\t{\n\t\t\t\t\taccept: \"application/json\",\n\t\t\t\t\t\"anthropic-dangerous-direct-browser-access\": \"true\",\n\t\t\t\t\t...(betaFeatures.length > 0 ? { \"anthropic-beta\": betaFeatures.join(\",\") } : {}),\n\t\t\t\t},\n\t\t\t\tmodel.headers,\n\t\t\t\tdynamicHeaders,\n\t\t\t\toptionsHeaders,\n\t\t\t),\n\t\t});\n\n\t\treturn { client, isOAuthToken: false };\n\t}\n\n\t// OAuth: Bearer auth, Claude Code identity headers\n\tif (apiKey && isOAuthToken(apiKey)) {\n\t\tconst client = new Anthropic({\n\t\t\tapiKey: null,\n\t\t\tauthToken: apiKey,\n\t\t\tbaseURL: model.baseUrl,\n\t\t\tdangerouslyAllowBrowser: true,\n\t\t\tdefaultHeaders: mergeHeaders(\n\t\t\t\t{\n\t\t\t\t\taccept: \"application/json\",\n\t\t\t\t\t\"anthropic-dangerous-direct-browser-access\": \"true\",\n\t\t\t\t\t\"anthropic-beta\": [\"claude-code-20250219\", \"oauth-2025-04-20\", ...betaFeatures].join(\",\"),\n\t\t\t\t\t\"user-agent\": `claude-cli/${claudeCodeVersion}`,\n\t\t\t\t\t\"x-app\": \"cli\",\n\t\t\t\t},\n\t\t\t\tmodel.headers,\n\t\t\t\toptionsHeaders,\n\t\t\t),\n\t\t});\n\n\t\treturn { client, isOAuthToken: true };\n\t}\n\n\t// API key or header-owned auth.\n\tconst sessionAffinityHeaders: ProviderHeaders =\n\t\tsessionId && getAnthropicCompat(model).sendSessionAffinityHeaders ? { \"x-session-affinity\": sessionId } : {};\n\tconst defaultHeaders = mergeHeaders(\n\t\t{\n\t\t\taccept: \"application/json\",\n\t\t\t\"anthropic-dangerous-direct-browser-access\": \"true\",\n\t\t\t...(betaFeatures.length > 0 ? { \"anthropic-beta\": betaFeatures.join(\",\") } : {}),\n\t\t},\n\t\tsessionAffinityHeaders,\n\t\tmodel.headers,\n\t\toptionsHeaders,\n\t);\n\tconst client = new Anthropic({\n\t\tapiKey: apiKey ?? null,\n\t\tauthToken: null,\n\t\tbaseURL: model.baseUrl,\n\t\tdangerouslyAllowBrowser: true,\n\t\tdefaultHeaders,\n\t});\n\n\treturn { client, isOAuthToken: false };\n}\n\nfunction buildParams(\n\tmodel: Model<\"anthropic-messages\">,\n\tcontext: Context,\n\tisOAuthToken: boolean,\n\toptions?: AnthropicOptions,\n): MessageCreateParamsStreaming {\n\tconst { cacheControl } = getCacheControl(model, options?.cacheRetention, options?.env);\n\tconst compat = getAnthropicCompat(model);\n\tconst params: MessageCreateParamsStreaming = {\n\t\tmodel: model.id,\n\t\tmessages: convertMessages(context.messages, model, isOAuthToken, cacheControl, compat.allowEmptySignature),\n\t\tmax_tokens: options?.maxTokens ?? model.maxTokens,\n\t\tstream: true,\n\t};\n\n\t// For OAuth tokens, we MUST include Claude Code identity\n\tif (isOAuthToken) {\n\t\tparams.system = [\n\t\t\t{\n\t\t\t\ttype: \"text\",\n\t\t\t\ttext: \"You are Claude Code, Anthropic's official CLI for Claude.\",\n\t\t\t\t...(cacheControl ? { cache_control: cacheControl } : {}),\n\t\t\t},\n\t\t];\n\t\tif (context.systemPrompt) {\n\t\t\tparams.system.push({\n\t\t\t\ttype: \"text\",\n\t\t\t\ttext: sanitizeSurrogates(context.systemPrompt),\n\t\t\t\t...(cacheControl ? { cache_control: cacheControl } : {}),\n\t\t\t});\n\t\t}\n\t} else if (context.systemPrompt) {\n\t\t// Add cache control to system prompt for non-OAuth tokens\n\t\tparams.system = [\n\t\t\t{\n\t\t\t\ttype: \"text\",\n\t\t\t\ttext: sanitizeSurrogates(context.systemPrompt),\n\t\t\t\t...(cacheControl ? { cache_control: cacheControl } : {}),\n\t\t\t},\n\t\t];\n\t}\n\n\t// Temperature is incompatible with extended thinking and unsupported on Claude Opus 4.7+.\n\tif (options?.temperature !== undefined && !options?.thinkingEnabled && compat.supportsTemperature) {\n\t\tparams.temperature = options.temperature;\n\t}\n\n\tif (context.tools && context.tools.length > 0) {\n\t\tparams.tools = convertTools(\n\t\t\tcontext.tools,\n\t\t\tisOAuthToken,\n\t\t\tcompat.supportsEagerToolInputStreaming,\n\t\t\tcompat.supportsCacheControlOnTools ? cacheControl : undefined,\n\t\t);\n\t}\n\n\t// Configure thinking mode: adaptive, budget-based, or explicitly disabled.\n\tif (model.reasoning) {\n\t\tif (options?.thinkingEnabled) {\n\t\t\t// Default to \"summarized\" so Opus 4.7 and Mythos Preview behave like\n\t\t\t// older Claude 4 models (whose API default is also \"summarized\").\n\t\t\tconst display: AnthropicThinkingDisplay = options.thinkingDisplay ?? \"summarized\";\n\t\t\tif (model.compat?.forceAdaptiveThinking === true) {\n\t\t\t\t// Adaptive thinking: Claude decides when and how much to think.\n\t\t\t\tparams.thinking = { type: \"adaptive\", display };\n\t\t\t\tif (options.effort) {\n\t\t\t\t\t// The Anthropic SDK types can lag newly supported effort values such as \"xhigh\".\n\t\t\t\t\tparams.output_config =\n\t\t\t\t\t\toptions.effort === \"xhigh\"\n\t\t\t\t\t\t\t? ({ effort: options.effort } as unknown as NonNullable<\n\t\t\t\t\t\t\t\t\tMessageCreateParamsStreaming[\"output_config\"]\n\t\t\t\t\t\t\t\t>)\n\t\t\t\t\t\t\t: { effort: options.effort };\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Budget-based thinking for older models\n\t\t\t\tparams.thinking = {\n\t\t\t\t\ttype: \"enabled\",\n\t\t\t\t\tbudget_tokens: options.thinkingBudgetTokens || 1024,\n\t\t\t\t\tdisplay,\n\t\t\t\t};\n\t\t\t}\n\t\t} else if (options?.thinkingEnabled === false && model.thinkingLevelMap?.off !== null) {\n\t\t\tparams.thinking = { type: \"disabled\" };\n\t\t}\n\t}\n\n\tif (options?.metadata) {\n\t\tconst userId = options.metadata.user_id;\n\t\tif (typeof userId === \"string\") {\n\t\t\tparams.metadata = { user_id: userId };\n\t\t}\n\t}\n\n\tif (options?.toolChoice) {\n\t\tif (typeof options.toolChoice === \"string\") {\n\t\t\tparams.tool_choice = { type: options.toolChoice };\n\t\t} else {\n\t\t\tparams.tool_choice = options.toolChoice;\n\t\t}\n\t}\n\n\treturn params;\n}\n\n// Normalize tool call IDs to match Anthropic's required pattern and length\nfunction normalizeToolCallId(id: string): string {\n\treturn id.replace(/[^a-zA-Z0-9_-]/g, \"_\").slice(0, 64);\n}\n\nfunction convertMessages(\n\tmessages: Message[],\n\tmodel: Model<\"anthropic-messages\">,\n\tisOAuthToken: boolean,\n\tcacheControl?: CacheControlEphemeral,\n\tallowEmptySignature = false,\n): MessageParam[] {\n\tconst params: MessageParam[] = [];\n\n\t// Transform messages for cross-provider compatibility\n\tconst transformedMessages = transformMessages(messages, model, normalizeToolCallId);\n\n\tfor (let i = 0; i < transformedMessages.length; i++) {\n\t\tconst msg = transformedMessages[i];\n\n\t\tif (msg.role === \"user\") {\n\t\t\tif (typeof msg.content === \"string\") {\n\t\t\t\tif (msg.content.trim().length > 0) {\n\t\t\t\t\tparams.push({\n\t\t\t\t\t\trole: \"user\",\n\t\t\t\t\t\tcontent: sanitizeSurrogates(msg.content),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst blocks: ContentBlockParam[] = msg.content.map((item) => {\n\t\t\t\t\tif (item.type === \"text\") {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: sanitizeSurrogates(item.text),\n\t\t\t\t\t\t};\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\ttype: \"image\",\n\t\t\t\t\t\t\tsource: {\n\t\t\t\t\t\t\t\ttype: \"base64\",\n\t\t\t\t\t\t\t\tmedia_type: item.mimeType as \"image/jpeg\" | \"image/png\" | \"image/gif\" | \"image/webp\",\n\t\t\t\t\t\t\t\tdata: item.data,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tconst filteredBlocks = blocks.filter((b) => {\n\t\t\t\t\tif (b.type === \"text\") {\n\t\t\t\t\t\treturn b.text.trim().length > 0;\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t});\n\t\t\t\tif (filteredBlocks.length === 0) continue;\n\t\t\t\tparams.push({\n\t\t\t\t\trole: \"user\",\n\t\t\t\t\tcontent: filteredBlocks,\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (msg.role === \"assistant\") {\n\t\t\tconst blocks: ContentBlockParam[] = [];\n\n\t\t\tfor (const block of msg.content) {\n\t\t\t\tif (block.type === \"text\") {\n\t\t\t\t\tif (block.text.trim().length === 0) continue;\n\t\t\t\t\tblocks.push({\n\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\ttext: sanitizeSurrogates(block.text),\n\t\t\t\t\t});\n\t\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\t\t// Redacted thinking: pass the opaque payload back as redacted_thinking\n\t\t\t\t\tif (block.redacted) {\n\t\t\t\t\t\tblocks.push({\n\t\t\t\t\t\t\ttype: \"redacted_thinking\",\n\t\t\t\t\t\t\tdata: block.thinkingSignature!,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst thinkingSignature = block.thinkingSignature;\n\t\t\t\t\tconst hasThinkingSignature = !!thinkingSignature && thinkingSignature.trim().length > 0;\n\t\t\t\t\tif (block.thinking.trim().length === 0 && !hasThinkingSignature) continue;\n\t\t\t\t\t// If thinking signature is missing/empty (e.g., from aborted stream),\n\t\t\t\t\t// convert to plain text for Anthropic. Some compatible providers emit\n\t\t\t\t\t// and accept empty signatures, so let marked models preserve the block.\n\t\t\t\t\tif (!hasThinkingSignature) {\n\t\t\t\t\t\tblocks.push(\n\t\t\t\t\t\t\tallowEmptySignature\n\t\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\t\t\t\t\t\tthinking: sanitizeSurrogates(block.thinking),\n\t\t\t\t\t\t\t\t\t\tsignature: \"\",\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\ttext: sanitizeSurrogates(block.thinking),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tblocks.push({\n\t\t\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\t\t\tthinking: sanitizeSurrogates(block.thinking),\n\t\t\t\t\t\t\tsignature: thinkingSignature,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\tblocks.push({\n\t\t\t\t\t\ttype: \"tool_use\",\n\t\t\t\t\t\tid: block.id,\n\t\t\t\t\t\tname: isOAuthToken ? toClaudeCodeName(block.name) : block.name,\n\t\t\t\t\t\tinput: block.arguments ?? {},\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (blocks.length === 0) continue;\n\t\t\tparams.push({\n\t\t\t\trole: \"assistant\",\n\t\t\t\tcontent: blocks,\n\t\t\t});\n\t\t} else if (msg.role === \"toolResult\") {\n\t\t\t// Collect all consecutive toolResult messages, needed for z.ai Anthropic endpoint\n\t\t\tconst toolResults: ContentBlockParam[] = [];\n\n\t\t\t// Add the current tool result\n\t\t\ttoolResults.push({\n\t\t\t\ttype: \"tool_result\",\n\t\t\t\ttool_use_id: msg.toolCallId,\n\t\t\t\tcontent: convertContentBlocks(msg.content),\n\t\t\t\tis_error: msg.isError,\n\t\t\t});\n\n\t\t\t// Look ahead for consecutive toolResult messages\n\t\t\tlet j = i + 1;\n\t\t\twhile (j < transformedMessages.length && transformedMessages[j].role === \"toolResult\") {\n\t\t\t\tconst nextMsg = transformedMessages[j] as ToolResultMessage; // We know it's a toolResult\n\t\t\t\ttoolResults.push({\n\t\t\t\t\ttype: \"tool_result\",\n\t\t\t\t\ttool_use_id: nextMsg.toolCallId,\n\t\t\t\t\tcontent: convertContentBlocks(nextMsg.content),\n\t\t\t\t\tis_error: nextMsg.isError,\n\t\t\t\t});\n\t\t\t\tj++;\n\t\t\t}\n\n\t\t\t// Skip the messages we've already processed\n\t\t\ti = j - 1;\n\n\t\t\t// Add a single user message with all tool results\n\t\t\tparams.push({\n\t\t\t\trole: \"user\",\n\t\t\t\tcontent: toolResults,\n\t\t\t});\n\t\t}\n\t}\n\n\t// Add cache_control to the last user message to cache conversation history\n\tif (cacheControl && params.length > 0) {\n\t\tconst lastMessage = params[params.length - 1];\n\t\tif (lastMessage.role === \"user\") {\n\t\t\tif (Array.isArray(lastMessage.content)) {\n\t\t\t\tconst lastBlock = lastMessage.content[lastMessage.content.length - 1];\n\t\t\t\tif (\n\t\t\t\t\tlastBlock &&\n\t\t\t\t\t(lastBlock.type === \"text\" || lastBlock.type === \"image\" || lastBlock.type === \"tool_result\")\n\t\t\t\t) {\n\t\t\t\t\t(lastBlock as any).cache_control = cacheControl;\n\t\t\t\t}\n\t\t\t} else if (typeof lastMessage.content === \"string\") {\n\t\t\t\tlastMessage.content = [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\ttext: lastMessage.content,\n\t\t\t\t\t\tcache_control: cacheControl,\n\t\t\t\t\t},\n\t\t\t\t] as any;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn params;\n}\n\nfunction shouldUseFineGrainedToolStreamingBeta(model: Model<\"anthropic-messages\">, context: Context): boolean {\n\treturn !!context.tools?.length && !getAnthropicCompat(model).supportsEagerToolInputStreaming;\n}\n\nfunction convertTools(\n\ttools: Tool[],\n\tisOAuthToken: boolean,\n\tsupportsEagerToolInputStreaming: boolean,\n\tcacheControl?: CacheControlEphemeral,\n): Anthropic.Messages.Tool[] {\n\tif (!tools) return [];\n\n\treturn tools.map((tool, index) => {\n\t\tconst schema = tool.parameters as { properties?: unknown; required?: string[] };\n\n\t\treturn {\n\t\t\tname: isOAuthToken ? toClaudeCodeName(tool.name) : tool.name,\n\t\t\tdescription: tool.description,\n\t\t\t...(supportsEagerToolInputStreaming ? { eager_input_streaming: true } : {}),\n\t\t\tinput_schema: {\n\t\t\t\ttype: \"object\",\n\t\t\t\tproperties: schema.properties ?? {},\n\t\t\t\trequired: schema.required ?? [],\n\t\t\t},\n\t\t\t...(cacheControl && index === tools.length - 1 ? { cache_control: cacheControl } : {}),\n\t\t};\n\t});\n}\n\nfunction mapStopReason(\n\treason: Anthropic.Messages.StopReason | string,\n\tstopDetails?: RefusalStopDetails | null,\n): { stopReason: StopReason; errorMessage?: string } {\n\tswitch (reason) {\n\t\tcase \"end_turn\":\n\t\t\treturn { stopReason: \"stop\" };\n\t\tcase \"max_tokens\":\n\t\t\treturn { stopReason: \"length\" };\n\t\tcase \"tool_use\":\n\t\t\treturn { stopReason: \"toolUse\" };\n\t\tcase \"refusal\":\n\t\t\treturn {\n\t\t\t\tstopReason: \"error\",\n\t\t\t\terrorMessage: stopDetails?.explanation || `The model refused to complete the request`,\n\t\t\t};\n\t\tcase \"pause_turn\": // Stop is good enough -> resubmit\n\t\t\treturn { stopReason: \"stop\" };\n\t\tcase \"stop_sequence\":\n\t\t\treturn { stopReason: \"stop\" }; // We don't supply stop sequences, so this should never happen\n\t\tcase \"sensitive\": // Content flagged by safety filters (not yet in SDK types)\n\t\t\treturn { stopReason: \"error\" };\n\t\tdefault:\n\t\t\t// Handle unknown stop reasons gracefully (API may add new values)\n\t\t\tthrow new Error(`Unhandled stop reason: ${reason}`);\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"anthropic-messages.d.ts","sourceRoot":"","sources":["../../src/api/anthropic-messages.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAU1C,OAAO,KAAK,EAWX,mBAAmB,EAEnB,cAAc,EACd,aAAa,EAMb,MAAM,aAAa,CAAC;AAqIrB,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;AAE1E,MAAM,MAAM,wBAAwB,GAAG,YAAY,GAAG,SAAS,CAAC;AAiChE,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACtD;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,wBAAwB,CAAC;IAC3C;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;CACnB;AAiOD,eAAO,MAAM,MAAM,EAAE,cAAc,CAAC,oBAAoB,EAAE,gBAAgB,CAmRzE,CAAC;AA2BF,eAAO,MAAM,YAAY,EAAE,cAAc,CAAC,oBAAoB,EAAE,mBAAmB,CAwClF,CAAC","sourcesContent":["import Anthropic from \"@anthropic-ai/sdk\";\nimport type {\n\tCacheControlEphemeral,\n\tContentBlockParam,\n\tMessageCreateParamsStreaming,\n\tMessageParam,\n\tRawMessageStreamEvent,\n\tRefusalStopDetails,\n} from \"@anthropic-ai/sdk/resources/messages.js\";\nimport { calculateCost } from \"../models.ts\";\nimport type {\n\tAnthropicMessagesCompat,\n\tApi,\n\tAssistantMessage,\n\tCacheRetention,\n\tContext,\n\tImageContent,\n\tMessage,\n\tModel,\n\tProviderEnv,\n\tProviderHeaders,\n\tSimpleStreamOptions,\n\tStopReason,\n\tStreamFunction,\n\tStreamOptions,\n\tTextContent,\n\tThinkingContent,\n\tTool,\n\tToolCall,\n\tToolResultMessage,\n} from \"../types.ts\";\nimport { splitDeferredTools } from \"../utils/deferred-tools.ts\";\nimport { AssistantMessageEventStream } from \"../utils/event-stream.ts\";\nimport { headersToRecord } from \"../utils/headers.ts\";\nimport { parseJsonWithRepair, parseStreamingJson } from \"../utils/json-parse.ts\";\nimport { getProviderEnvValue } from \"../utils/provider-env.ts\";\nimport { sanitizeSurrogates } from \"../utils/sanitize-unicode.ts\";\n\nimport { buildCopilotDynamicHeaders, hasCopilotVisionInput } from \"./github-copilot-headers.ts\";\nimport { adjustMaxTokensForThinking, buildBaseOptions, clampMaxTokensToContext } from \"./simple-options.ts\";\nimport { transformMessages } from \"./transform-messages.ts\";\n\n/**\n * Resolve cache retention preference.\n * Defaults to \"short\" and uses PI_CACHE_RETENTION for backward compatibility.\n */\nfunction resolveCacheRetention(cacheRetention?: CacheRetention, env?: ProviderEnv): CacheRetention {\n\tif (cacheRetention) {\n\t\treturn cacheRetention;\n\t}\n\tif (getProviderEnvValue(\"PI_CACHE_RETENTION\", env) === \"long\") {\n\t\treturn \"long\";\n\t}\n\treturn \"short\";\n}\n\nfunction getCacheControl(\n\tmodel: Model<\"anthropic-messages\">,\n\tcacheRetention?: CacheRetention,\n\tenv?: ProviderEnv,\n): { retention: CacheRetention; cacheControl?: CacheControlEphemeral } {\n\tconst retention = resolveCacheRetention(cacheRetention, env);\n\tif (retention === \"none\") {\n\t\treturn { retention };\n\t}\n\tconst ttl = retention === \"long\" && getAnthropicCompat(model).supportsLongCacheRetention ? \"1h\" : undefined;\n\treturn {\n\t\tretention,\n\t\tcacheControl: { type: \"ephemeral\", ...(ttl && { ttl }) },\n\t};\n}\n\n// Stealth mode: Mimic Claude Code's tool naming exactly\nconst claudeCodeVersion = \"2.1.75\";\n\n// Claude Code 2.x tool names (canonical casing)\n// Source: https://cchistory.mariozechner.at/data/prompts-2.1.11.md\n// To update: https://github.com/badlogic/cchistory\nconst claudeCodeTools = [\n\t\"Read\",\n\t\"Write\",\n\t\"Edit\",\n\t\"Bash\",\n\t\"Grep\",\n\t\"Glob\",\n\t\"AskUserQuestion\",\n\t\"EnterPlanMode\",\n\t\"ExitPlanMode\",\n\t\"KillShell\",\n\t\"NotebookEdit\",\n\t\"Skill\",\n\t\"Task\",\n\t\"TaskOutput\",\n\t\"TodoWrite\",\n\t\"WebFetch\",\n\t\"WebSearch\",\n];\n\nconst ccToolLookup = new Map(claudeCodeTools.map((t) => [t.toLowerCase(), t]));\n\n// Convert tool name to CC canonical casing if it matches (case-insensitive)\nconst toClaudeCodeName = (name: string) => ccToolLookup.get(name.toLowerCase()) ?? name;\nconst fromClaudeCodeName = (name: string, tools?: Tool[]) => {\n\tif (tools && tools.length > 0) {\n\t\tconst lowerName = name.toLowerCase();\n\t\tconst matchedTool = tools.find((tool) => tool.name.toLowerCase() === lowerName);\n\t\tif (matchedTool) return matchedTool.name;\n\t}\n\treturn name;\n};\n\n/**\n * Convert content blocks to Anthropic API format\n */\nfunction convertContentBlocks(content: (TextContent | ImageContent)[]):\n\t| string\n\t| Array<\n\t\t\t| { type: \"text\"; text: string }\n\t\t\t| {\n\t\t\t\t\ttype: \"image\";\n\t\t\t\t\tsource: {\n\t\t\t\t\t\ttype: \"base64\";\n\t\t\t\t\t\tmedia_type: \"image/jpeg\" | \"image/png\" | \"image/gif\" | \"image/webp\";\n\t\t\t\t\t\tdata: string;\n\t\t\t\t\t};\n\t\t\t }\n\t > {\n\t// If only text blocks, return as concatenated string for simplicity\n\tconst hasImages = content.some((c) => c.type === \"image\");\n\tif (!hasImages) {\n\t\treturn sanitizeSurrogates(content.map((c) => (c as TextContent).text).join(\"\\n\"));\n\t}\n\n\t// If we have images, convert to content block array\n\tconst blocks = content.map((block) => {\n\t\tif (block.type === \"text\") {\n\t\t\treturn {\n\t\t\t\ttype: \"text\" as const,\n\t\t\t\ttext: sanitizeSurrogates(block.text),\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\ttype: \"image\" as const,\n\t\t\tsource: {\n\t\t\t\ttype: \"base64\" as const,\n\t\t\t\tmedia_type: block.mimeType as \"image/jpeg\" | \"image/png\" | \"image/gif\" | \"image/webp\",\n\t\t\t\tdata: block.data,\n\t\t\t},\n\t\t};\n\t});\n\n\t// If only images (no text), add placeholder text block\n\tconst hasText = blocks.some((b) => b.type === \"text\");\n\tif (!hasText) {\n\t\tblocks.unshift({\n\t\t\ttype: \"text\" as const,\n\t\t\ttext: \"(see attached image)\",\n\t\t});\n\t}\n\n\treturn blocks;\n}\n\nexport type AnthropicEffort = \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\";\n\nexport type AnthropicThinkingDisplay = \"summarized\" | \"omitted\";\n\nconst FINE_GRAINED_TOOL_STREAMING_BETA = \"fine-grained-tool-streaming-2025-05-14\";\nconst INTERLEAVED_THINKING_BETA = \"interleaved-thinking-2025-05-14\";\n\nfunction getAnthropicCompat(\n\tmodel: Model<\"anthropic-messages\">,\n): Required<Omit<AnthropicMessagesCompat, \"forceAdaptiveThinking\">> {\n\treturn {\n\t\tsupportsEagerToolInputStreaming: model.compat?.supportsEagerToolInputStreaming ?? true,\n\t\tsupportsLongCacheRetention: model.compat?.supportsLongCacheRetention ?? true,\n\t\tsendSessionAffinityHeaders: model.compat?.sendSessionAffinityHeaders ?? false,\n\t\tsupportsCacheControlOnTools: model.compat?.supportsCacheControlOnTools ?? true,\n\t\tsupportsTemperature: model.compat?.supportsTemperature ?? true,\n\t\tallowEmptySignature: model.compat?.allowEmptySignature ?? false,\n\t\tsupportsToolReferences: model.compat?.supportsToolReferences ?? defaultSupportsToolReferences(model),\n\t};\n}\n\n/**\n * Default for `supportsToolReferences`: first-party Anthropic models except\n * Haiku (rejects client-side tool_reference blocks) and models that predate\n * tool search (Claude 3.x, Opus/Sonnet 4.0, Opus 4.1).\n */\nfunction defaultSupportsToolReferences(model: Model<\"anthropic-messages\">): boolean {\n\tif (model.provider !== \"anthropic\" || model.id.includes(\"haiku\")) return false;\n\tconst version = model.id.match(/^claude-(?:opus|sonnet|fable)-(\\d+)(?:-(\\d+))?(?:-|$)/);\n\tif (!version) return false;\n\tconst major = Number(version[1]);\n\tconst minor = version[2] && version[2].length < 8 ? Number(version[2]) : 0;\n\treturn major > 4 || (major === 4 && minor >= 5);\n}\n\nexport interface AnthropicOptions extends StreamOptions {\n\t/**\n\t * Enable extended thinking.\n\t * For adaptive thinking models: the model decides when/how much to think.\n\t * For older models: uses budget-based thinking with thinkingBudgetTokens.\n\t * Default: undefined (thinking is omitted unless `streamSimple()` maps\n\t * a simple reasoning level to this option, or callers set it explicitly).\n\t */\n\tthinkingEnabled?: boolean;\n\t/**\n\t * Token budget for extended thinking (older models only).\n\t * Ignored for adaptive thinking models.\n\t * Default: 1024 when `thinkingEnabled` is true and no budget is provided.\n\t */\n\tthinkingBudgetTokens?: number;\n\t/**\n\t * Effort level for adaptive thinking models.\n\t * Controls how much thinking Claude allocates:\n\t * - \"max\": Always thinks with no constraints (Opus 4.6 only)\n\t * - \"xhigh\": Highest reasoning level (Opus 4.7+, Fable 5)\n\t * - \"high\": Always thinks, deep reasoning\n\t * - \"medium\": Moderate thinking, may skip for simple queries\n\t * - \"low\": Minimal thinking, skips for simple tasks\n\t * Ignored for older models.\n\t * Default: omitted unless `streamSimple()` maps a simple reasoning\n\t * level to this option.\n\t */\n\teffort?: AnthropicEffort;\n\t/**\n\t * Controls how thinking content is returned in API responses.\n\t * - \"summarized\": Thinking blocks contain summarized thinking text.\n\t * - \"omitted\": Thinking blocks return an empty thinking field; the encrypted\n\t * signature still travels back for multi-turn continuity. Use for faster\n\t * time-to-first-text-token when your UI does not surface thinking.\n\t *\n\t * Note: Anthropic's API default for Claude Opus 4.7 and Claude Mythos Preview\n\t * is \"omitted\". We default to \"summarized\" here to keep behavior consistent\n\t * with older Claude 4 models. Set this explicitly to \"omitted\" to opt in.\n\t * Default: \"summarized\" when thinking is enabled.\n\t */\n\tthinkingDisplay?: AnthropicThinkingDisplay;\n\t/**\n\t * Whether to request the interleaved thinking beta header for non-adaptive\n\t * thinking models. Adaptive thinking models have interleaved thinking built in,\n\t * so the header is skipped for them regardless of this setting.\n\t * Default: true.\n\t */\n\tinterleavedThinking?: boolean;\n\t/**\n\t * Anthropic tool choice behavior. String values map to Anthropic's built-in\n\t * choices; `{ type: \"tool\", name }` forces a specific tool.\n\t * Default: omitted (Anthropic default behavior, currently equivalent to auto).\n\t */\n\ttoolChoice?: \"auto\" | \"any\" | \"none\" | { type: \"tool\"; name: string };\n\t/**\n\t * Pre-built Anthropic client instance. When provided, skips internal client\n\t * construction entirely. Use this to inject alternative SDK clients such as\n\t * `AnthropicVertex` that shares the same messaging API.\n\t */\n\tclient?: Anthropic;\n}\n\nfunction mergeHeaders(...headerSources: (ProviderHeaders | undefined)[]): ProviderHeaders {\n\tconst merged: ProviderHeaders = {};\n\tfor (const headers of headerSources) {\n\t\tif (headers) {\n\t\t\tObject.assign(merged, headers);\n\t\t}\n\t}\n\treturn merged;\n}\n\nfunction hasHeader(headers: ProviderHeaders | undefined, name: string): boolean {\n\tif (!headers) return false;\n\tconst expected = name.toLowerCase();\n\tfor (const [key, value] of Object.entries(headers)) {\n\t\tif (key.toLowerCase() === expected && value !== null && value.trim().length > 0) return true;\n\t}\n\treturn false;\n}\n\nfunction assertRequestAuth(provider: string, apiKey: string | undefined, headers: ProviderHeaders | undefined): void {\n\tif (apiKey) return;\n\tif (\n\t\thasHeader(headers, \"authorization\") ||\n\t\thasHeader(headers, \"x-api-key\") ||\n\t\thasHeader(headers, \"cf-aig-authorization\")\n\t) {\n\t\treturn;\n\t}\n\tthrow new Error(`No API key for provider: ${provider}`);\n}\n\ninterface ServerSentEvent {\n\tevent: string | null;\n\tdata: string;\n\traw: string[];\n}\n\ninterface SseDecoderState {\n\tevent: string | null;\n\tdata: string[];\n\traw: string[];\n}\n\nconst ANTHROPIC_MESSAGE_EVENTS: ReadonlySet<string> = new Set([\n\t\"message_start\",\n\t\"message_delta\",\n\t\"message_stop\",\n\t\"content_block_start\",\n\t\"content_block_delta\",\n\t\"content_block_stop\",\n]);\n\nfunction flushSseEvent(state: SseDecoderState): ServerSentEvent | null {\n\tif (!state.event && state.data.length === 0) {\n\t\treturn null;\n\t}\n\n\tconst event: ServerSentEvent = {\n\t\tevent: state.event,\n\t\tdata: state.data.join(\"\\n\"),\n\t\traw: [...state.raw],\n\t};\n\tstate.event = null;\n\tstate.data = [];\n\tstate.raw = [];\n\treturn event;\n}\n\nfunction decodeSseLine(line: string, state: SseDecoderState): ServerSentEvent | null {\n\tif (line === \"\") {\n\t\treturn flushSseEvent(state);\n\t}\n\n\tstate.raw.push(line);\n\tif (line.startsWith(\":\")) {\n\t\treturn null;\n\t}\n\n\tconst delimiterIndex = line.indexOf(\":\");\n\tconst fieldName = delimiterIndex === -1 ? line : line.slice(0, delimiterIndex);\n\tlet value = delimiterIndex === -1 ? \"\" : line.slice(delimiterIndex + 1);\n\tif (value.startsWith(\" \")) {\n\t\tvalue = value.slice(1);\n\t}\n\n\tif (fieldName === \"event\") {\n\t\tstate.event = value;\n\t} else if (fieldName === \"data\") {\n\t\tstate.data.push(value);\n\t}\n\n\treturn null;\n}\n\nfunction nextLineBreakIndex(text: string): number {\n\tconst carriageReturnIndex = text.indexOf(\"\\r\");\n\tconst newlineIndex = text.indexOf(\"\\n\");\n\tif (carriageReturnIndex === -1) {\n\t\treturn newlineIndex;\n\t}\n\tif (newlineIndex === -1) {\n\t\treturn carriageReturnIndex;\n\t}\n\treturn Math.min(carriageReturnIndex, newlineIndex);\n}\n\nfunction consumeLine(text: string): { line: string; rest: string } | null {\n\tconst lineBreakIndex = nextLineBreakIndex(text);\n\tif (lineBreakIndex === -1) {\n\t\treturn null;\n\t}\n\n\tlet nextIndex = lineBreakIndex + 1;\n\tif (text[lineBreakIndex] === \"\\r\" && text[nextIndex] === \"\\n\") {\n\t\tnextIndex += 1;\n\t}\n\n\treturn {\n\t\tline: text.slice(0, lineBreakIndex),\n\t\trest: text.slice(nextIndex),\n\t};\n}\n\nasync function* iterateSseMessages(\n\tbody: ReadableStream<Uint8Array>,\n\tsignal?: AbortSignal,\n): AsyncGenerator<ServerSentEvent> {\n\tconst reader = body.getReader();\n\tconst decoder = new TextDecoder();\n\tconst state: SseDecoderState = { event: null, data: [], raw: [] };\n\tlet buffer = \"\";\n\n\ttry {\n\t\twhile (true) {\n\t\t\tif (signal?.aborted) {\n\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t}\n\n\t\t\tconst { value, done } = await reader.read();\n\t\t\tif (done) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tbuffer += decoder.decode(value, { stream: true });\n\t\t\tlet consumed = consumeLine(buffer);\n\t\t\twhile (consumed) {\n\t\t\t\tbuffer = consumed.rest;\n\t\t\t\tconst event = decodeSseLine(consumed.line, state);\n\t\t\t\tif (event) {\n\t\t\t\t\tyield event;\n\t\t\t\t}\n\t\t\t\tconsumed = consumeLine(buffer);\n\t\t\t}\n\t\t}\n\n\t\tbuffer += decoder.decode();\n\t\tlet consumed = consumeLine(buffer);\n\t\twhile (consumed) {\n\t\t\tbuffer = consumed.rest;\n\t\t\tconst event = decodeSseLine(consumed.line, state);\n\t\t\tif (event) {\n\t\t\t\tyield event;\n\t\t\t}\n\t\t\tconsumed = consumeLine(buffer);\n\t\t}\n\n\t\tif (buffer.length > 0) {\n\t\t\tconst event = decodeSseLine(buffer, state);\n\t\t\tif (event) {\n\t\t\t\tyield event;\n\t\t\t}\n\t\t}\n\n\t\tconst trailingEvent = flushSseEvent(state);\n\t\tif (trailingEvent) {\n\t\t\tyield trailingEvent;\n\t\t}\n\t} finally {\n\t\treader.releaseLock();\n\t}\n}\n\nasync function* iterateAnthropicEvents(\n\tresponse: Response,\n\tsignal?: AbortSignal,\n): AsyncGenerator<RawMessageStreamEvent> {\n\tif (!response.body) {\n\t\tthrow new Error(\"Attempted to iterate over an Anthropic response with no body\");\n\t}\n\n\tlet sawMessageStart = false;\n\tlet sawMessageEnd = false;\n\n\tfor await (const sse of iterateSseMessages(response.body, signal)) {\n\t\tif (sse.event === \"error\") {\n\t\t\tthrow new Error(sse.data);\n\t\t}\n\n\t\tif (!ANTHROPIC_MESSAGE_EVENTS.has(sse.event ?? \"\")) {\n\t\t\tcontinue;\n\t\t}\n\n\t\ttry {\n\t\t\tconst event = parseJsonWithRepair<RawMessageStreamEvent>(sse.data);\n\t\t\tif (event.type === \"message_start\") {\n\t\t\t\tsawMessageStart = true;\n\t\t\t} else if (event.type === \"message_stop\") {\n\t\t\t\tsawMessageEnd = true;\n\t\t\t}\n\t\t\tyield event;\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\tthrow new Error(\n\t\t\t\t`Could not parse Anthropic SSE event ${sse.event}: ${message}; data=${sse.data}; raw=${sse.raw.join(\"\\\\n\")}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tif (sawMessageStart && !sawMessageEnd) {\n\t\tthrow new Error(\"Anthropic stream ended before message_stop\");\n\t}\n}\n\nexport const stream: StreamFunction<\"anthropic-messages\", AnthropicOptions> = (\n\tmodel: Model<\"anthropic-messages\">,\n\tcontext: Context,\n\toptions?: AnthropicOptions,\n): AssistantMessageEventStream => {\n\tconst stream = new AssistantMessageEventStream();\n\n\t(async () => {\n\t\tconst output: AssistantMessage = {\n\t\t\trole: \"assistant\",\n\t\t\tcontent: [],\n\t\t\tapi: model.api as Api,\n\t\t\tprovider: model.provider,\n\t\t\tmodel: model.id,\n\t\t\tusage: {\n\t\t\t\tinput: 0,\n\t\t\t\toutput: 0,\n\t\t\t\tcacheRead: 0,\n\t\t\t\tcacheWrite: 0,\n\t\t\t\ttotalTokens: 0,\n\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t\t},\n\t\t\tstopReason: \"stop\",\n\t\t\ttimestamp: Date.now(),\n\t\t};\n\n\t\ttry {\n\t\t\tlet client: Anthropic;\n\t\t\tlet isOAuth: boolean;\n\n\t\t\tif (options?.client) {\n\t\t\t\tclient = options.client;\n\t\t\t\tisOAuth = false;\n\t\t\t} else {\n\t\t\t\tconst apiKey = options?.apiKey;\n\t\t\t\tassertRequestAuth(model.provider, apiKey, options?.headers);\n\n\t\t\t\tlet copilotDynamicHeaders: Record<string, string> | undefined;\n\t\t\t\tif (model.provider === \"github-copilot\") {\n\t\t\t\t\tconst hasImages = hasCopilotVisionInput(context.messages);\n\t\t\t\t\tcopilotDynamicHeaders = buildCopilotDynamicHeaders({\n\t\t\t\t\t\tmessages: context.messages,\n\t\t\t\t\t\thasImages,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tconst cacheRetention = resolveCacheRetention(options?.cacheRetention, options?.env);\n\t\t\t\tconst cacheSessionId = cacheRetention === \"none\" ? undefined : options?.sessionId;\n\n\t\t\t\tconst created = createClient(\n\t\t\t\t\tmodel,\n\t\t\t\t\tapiKey,\n\t\t\t\t\toptions?.interleavedThinking ?? true,\n\t\t\t\t\tshouldUseFineGrainedToolStreamingBeta(model, context),\n\t\t\t\t\toptions?.headers,\n\t\t\t\t\tcopilotDynamicHeaders,\n\t\t\t\t\tcacheSessionId,\n\t\t\t\t);\n\t\t\t\tclient = created.client;\n\t\t\t\tisOAuth = created.isOAuthToken;\n\t\t\t}\n\t\t\tlet params = buildParams(model, context, isOAuth, options);\n\t\t\tconst nextParams = await options?.onPayload?.(params, model);\n\t\t\tif (nextParams !== undefined) {\n\t\t\t\tparams = nextParams as MessageCreateParamsStreaming;\n\t\t\t}\n\t\t\tconst requestOptions = {\n\t\t\t\t...(options?.signal ? { signal: options.signal } : {}),\n\t\t\t\t...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),\n\t\t\t\tmaxRetries: options?.maxRetries ?? 0,\n\t\t\t};\n\t\t\tconst response = await client.messages.create({ ...params, stream: true }, requestOptions).asResponse();\n\t\t\tawait options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);\n\t\t\tstream.push({ type: \"start\", partial: output });\n\n\t\t\ttype Block = (ThinkingContent | TextContent | (ToolCall & { partialJson: string })) & { index: number };\n\t\t\tconst blocks = output.content as Block[];\n\n\t\t\tfor await (const event of iterateAnthropicEvents(response, options?.signal)) {\n\t\t\t\tif (event.type === \"message_start\") {\n\t\t\t\t\toutput.responseId = event.message.id;\n\t\t\t\t\t// Capture initial token usage from message_start event\n\t\t\t\t\t// This ensures we have input token counts even if the stream is aborted early\n\t\t\t\t\toutput.usage.input = event.message.usage.input_tokens || 0;\n\t\t\t\t\toutput.usage.output = event.message.usage.output_tokens || 0;\n\t\t\t\t\toutput.usage.cacheRead = event.message.usage.cache_read_input_tokens || 0;\n\t\t\t\t\toutput.usage.cacheWrite = event.message.usage.cache_creation_input_tokens || 0;\n\t\t\t\t\toutput.usage.cacheWrite1h = event.message.usage.cache_creation?.ephemeral_1h_input_tokens || 0;\n\t\t\t\t\t// Anthropic doesn't provide total_tokens, compute from components\n\t\t\t\t\toutput.usage.totalTokens =\n\t\t\t\t\t\toutput.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;\n\t\t\t\t\tcalculateCost(model, output.usage);\n\t\t\t\t} else if (event.type === \"content_block_start\") {\n\t\t\t\t\tif (event.content_block.type === \"text\") {\n\t\t\t\t\t\tconst block: Block = {\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"\",\n\t\t\t\t\t\t\tindex: event.index,\n\t\t\t\t\t\t};\n\t\t\t\t\t\toutput.content.push(block);\n\t\t\t\t\t\tstream.push({ type: \"text_start\", contentIndex: output.content.length - 1, partial: output });\n\t\t\t\t\t} else if (event.content_block.type === \"thinking\") {\n\t\t\t\t\t\tconst block: Block = {\n\t\t\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\t\t\tthinking: \"\",\n\t\t\t\t\t\t\tthinkingSignature: \"\",\n\t\t\t\t\t\t\tindex: event.index,\n\t\t\t\t\t\t};\n\t\t\t\t\t\toutput.content.push(block);\n\t\t\t\t\t\tstream.push({ type: \"thinking_start\", contentIndex: output.content.length - 1, partial: output });\n\t\t\t\t\t} else if (event.content_block.type === \"redacted_thinking\") {\n\t\t\t\t\t\tconst block: Block = {\n\t\t\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\t\t\tthinking: \"[Reasoning redacted]\",\n\t\t\t\t\t\t\tthinkingSignature: event.content_block.data,\n\t\t\t\t\t\t\tredacted: true,\n\t\t\t\t\t\t\tindex: event.index,\n\t\t\t\t\t\t};\n\t\t\t\t\t\toutput.content.push(block);\n\t\t\t\t\t\tstream.push({ type: \"thinking_start\", contentIndex: output.content.length - 1, partial: output });\n\t\t\t\t\t} else if (event.content_block.type === \"tool_use\") {\n\t\t\t\t\t\tconst block: Block = {\n\t\t\t\t\t\t\ttype: \"toolCall\",\n\t\t\t\t\t\t\tid: event.content_block.id,\n\t\t\t\t\t\t\tname: isOAuth\n\t\t\t\t\t\t\t\t? fromClaudeCodeName(event.content_block.name, context.tools)\n\t\t\t\t\t\t\t\t: event.content_block.name,\n\t\t\t\t\t\t\targuments: (event.content_block.input as Record<string, any>) ?? {},\n\t\t\t\t\t\t\tpartialJson: \"\",\n\t\t\t\t\t\t\tindex: event.index,\n\t\t\t\t\t\t};\n\t\t\t\t\t\toutput.content.push(block);\n\t\t\t\t\t\tstream.push({ type: \"toolcall_start\", contentIndex: output.content.length - 1, partial: output });\n\t\t\t\t\t}\n\t\t\t\t} else if (event.type === \"content_block_delta\") {\n\t\t\t\t\tif (event.delta.type === \"text_delta\") {\n\t\t\t\t\t\tconst index = blocks.findIndex((b) => b.index === event.index);\n\t\t\t\t\t\tconst block = blocks[index];\n\t\t\t\t\t\tif (block && block.type === \"text\") {\n\t\t\t\t\t\t\tblock.text += event.delta.text;\n\t\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\t\ttype: \"text_delta\",\n\t\t\t\t\t\t\t\tcontentIndex: index,\n\t\t\t\t\t\t\t\tdelta: event.delta.text,\n\t\t\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (event.delta.type === \"thinking_delta\") {\n\t\t\t\t\t\tconst index = blocks.findIndex((b) => b.index === event.index);\n\t\t\t\t\t\tconst block = blocks[index];\n\t\t\t\t\t\tif (block && block.type === \"thinking\") {\n\t\t\t\t\t\t\tblock.thinking += event.delta.thinking;\n\t\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\t\ttype: \"thinking_delta\",\n\t\t\t\t\t\t\t\tcontentIndex: index,\n\t\t\t\t\t\t\t\tdelta: event.delta.thinking,\n\t\t\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (event.delta.type === \"input_json_delta\") {\n\t\t\t\t\t\tconst index = blocks.findIndex((b) => b.index === event.index);\n\t\t\t\t\t\tconst block = blocks[index];\n\t\t\t\t\t\tif (block && block.type === \"toolCall\") {\n\t\t\t\t\t\t\tblock.partialJson += event.delta.partial_json;\n\t\t\t\t\t\t\tblock.arguments = parseStreamingJson(block.partialJson);\n\t\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\t\ttype: \"toolcall_delta\",\n\t\t\t\t\t\t\t\tcontentIndex: index,\n\t\t\t\t\t\t\t\tdelta: event.delta.partial_json,\n\t\t\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (event.delta.type === \"signature_delta\") {\n\t\t\t\t\t\tconst index = blocks.findIndex((b) => b.index === event.index);\n\t\t\t\t\t\tconst block = blocks[index];\n\t\t\t\t\t\tif (block && block.type === \"thinking\") {\n\t\t\t\t\t\t\tblock.thinkingSignature = block.thinkingSignature || \"\";\n\t\t\t\t\t\t\tblock.thinkingSignature += event.delta.signature;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if (event.type === \"content_block_stop\") {\n\t\t\t\t\tconst index = blocks.findIndex((b) => b.index === event.index);\n\t\t\t\t\tconst block = blocks[index];\n\t\t\t\t\tif (block) {\n\t\t\t\t\t\tdelete (block as any).index;\n\t\t\t\t\t\tif (block.type === \"text\") {\n\t\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\t\ttype: \"text_end\",\n\t\t\t\t\t\t\t\tcontentIndex: index,\n\t\t\t\t\t\t\t\tcontent: block.text,\n\t\t\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\t\ttype: \"thinking_end\",\n\t\t\t\t\t\t\t\tcontentIndex: index,\n\t\t\t\t\t\t\t\tcontent: block.thinking,\n\t\t\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\t\t\tblock.arguments = parseStreamingJson(block.partialJson);\n\t\t\t\t\t\t\t// Finalize in-place and strip the scratch buffer so replay only\n\t\t\t\t\t\t\t// carries parsed arguments.\n\t\t\t\t\t\t\tdelete (block as { partialJson?: string }).partialJson;\n\t\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\t\ttype: \"toolcall_end\",\n\t\t\t\t\t\t\t\tcontentIndex: index,\n\t\t\t\t\t\t\t\ttoolCall: block,\n\t\t\t\t\t\t\t\tpartial: output,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else if (event.type === \"message_delta\") {\n\t\t\t\t\tif (event.delta.stop_reason) {\n\t\t\t\t\t\tconst stopReasonResult = mapStopReason(event.delta.stop_reason, event.delta.stop_details);\n\t\t\t\t\t\toutput.stopReason = stopReasonResult.stopReason;\n\t\t\t\t\t\tif (stopReasonResult.errorMessage) {\n\t\t\t\t\t\t\toutput.errorMessage = stopReasonResult.errorMessage;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t// Only update usage fields if present (not null).\n\t\t\t\t\t// Preserves input_tokens from message_start when proxies omit it in message_delta.\n\t\t\t\t\tif (event.usage) {\n\t\t\t\t\t\tif (event.usage.input_tokens != null) {\n\t\t\t\t\t\t\toutput.usage.input = event.usage.input_tokens;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (event.usage.output_tokens != null) {\n\t\t\t\t\t\t\toutput.usage.output = event.usage.output_tokens;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (event.usage.cache_read_input_tokens != null) {\n\t\t\t\t\t\t\toutput.usage.cacheRead = event.usage.cache_read_input_tokens;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (event.usage.cache_creation_input_tokens != null) {\n\t\t\t\t\t\t\toutput.usage.cacheWrite = event.usage.cache_creation_input_tokens;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Anthropic reports reasoning tokens in `output_tokens_details.thinking_tokens` on the\n\t\t\t\t\t\t// final message_delta usage (a subset of output_tokens). SDK 0.91.1 omits the field from\n\t\t\t\t\t\t// its Usage type, so read it through a narrow cast. Verified against the live API.\n\t\t\t\t\t\tconst thinkingTokens = (event.usage as { output_tokens_details?: { thinking_tokens?: number } })\n\t\t\t\t\t\t\t.output_tokens_details?.thinking_tokens;\n\t\t\t\t\t\tif (thinkingTokens != null) {\n\t\t\t\t\t\t\toutput.usage.reasoning = thinkingTokens;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t// Anthropic doesn't provide total_tokens, compute from components\n\t\t\t\t\toutput.usage.totalTokens =\n\t\t\t\t\t\toutput.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;\n\t\t\t\t\tcalculateCost(model, output.usage);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (options?.signal?.aborted) {\n\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t}\n\n\t\t\tif (output.stopReason === \"aborted\" || output.stopReason === \"error\") {\n\t\t\t\tthrow new Error(output.errorMessage || \"An unknown error occurred\");\n\t\t\t}\n\n\t\t\tstream.push({ type: \"done\", reason: output.stopReason, message: output });\n\t\t\tstream.end();\n\t\t} catch (error) {\n\t\t\tfor (const block of output.content) {\n\t\t\t\tdelete (block as { index?: number }).index;\n\t\t\t\t// partialJson is only a streaming scratch buffer; never persist it.\n\t\t\t\tdelete (block as { partialJson?: string }).partialJson;\n\t\t\t}\n\t\t\toutput.stopReason = options?.signal?.aborted ? \"aborted\" : \"error\";\n\t\t\toutput.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);\n\t\t\tstream.push({ type: \"error\", reason: output.stopReason, error: output });\n\t\t\tstream.end();\n\t\t}\n\t})();\n\n\treturn stream;\n};\n\n/**\n * Map ThinkingLevel to Anthropic effort levels for adaptive thinking.\n * Note: effort \"max\" is available on all adaptive-thinking Claude models, while native\n * \"xhigh\" is only available on Opus 4.7/4.8, Sonnet 5, and Fable 5.\n */\nfunction mapThinkingLevelToEffort(\n\tmodel: Model<\"anthropic-messages\">,\n\tlevel: SimpleStreamOptions[\"reasoning\"],\n): AnthropicEffort {\n\tconst mapped = level ? model.thinkingLevelMap?.[level] : undefined;\n\tif (typeof mapped === \"string\") return mapped as AnthropicEffort;\n\n\tswitch (level) {\n\t\tcase \"minimal\":\n\t\tcase \"low\":\n\t\t\treturn \"low\";\n\t\tcase \"medium\":\n\t\t\treturn \"medium\";\n\t\tcase \"high\":\n\t\t\treturn \"high\";\n\t\tdefault:\n\t\t\treturn \"high\";\n\t}\n}\n\nexport const streamSimple: StreamFunction<\"anthropic-messages\", SimpleStreamOptions> = (\n\tmodel: Model<\"anthropic-messages\">,\n\tcontext: Context,\n\toptions?: SimpleStreamOptions,\n): AssistantMessageEventStream => {\n\tassertRequestAuth(model.provider, options?.apiKey, options?.headers);\n\n\tconst base = buildBaseOptions(model, context, options, options?.apiKey);\n\tif (!options?.reasoning) {\n\t\treturn stream(model, context, { ...base, thinkingEnabled: false } satisfies AnthropicOptions);\n\t}\n\n\t// For models with adaptive thinking: use an effort level.\n\t// For older models: use budget-based thinking.\n\tif (model.compat?.forceAdaptiveThinking === true) {\n\t\tconst effort = mapThinkingLevelToEffort(model, options.reasoning);\n\t\treturn stream(model, context, {\n\t\t\t...base,\n\t\t\tthinkingEnabled: true,\n\t\t\teffort,\n\t\t} satisfies AnthropicOptions);\n\t}\n\n\t// Undefined means the caller did not request an output cap; let the helper use the model cap.\n\t// Do not coerce to 0 here, or the thinking budget would become the entire max_tokens value.\n\tconst adjusted = adjustMaxTokensForThinking(\n\t\tbase.maxTokens,\n\t\tmodel.maxTokens,\n\t\toptions.reasoning,\n\t\toptions.thinkingBudgets,\n\t);\n\n\tconst maxTokens = clampMaxTokensToContext(model, context, adjusted.maxTokens);\n\n\treturn stream(model, context, {\n\t\t...base,\n\t\tmaxTokens,\n\t\tthinkingEnabled: true,\n\t\tthinkingBudgetTokens: Math.min(adjusted.thinkingBudget, Math.max(0, maxTokens - 1024)),\n\t} satisfies AnthropicOptions);\n};\n\nfunction isOAuthToken(apiKey: string): boolean {\n\treturn apiKey.includes(\"sk-ant-oat\");\n}\n\nfunction createClient(\n\tmodel: Model<\"anthropic-messages\">,\n\tapiKey: string | undefined,\n\tinterleavedThinking: boolean,\n\tuseFineGrainedToolStreamingBeta: boolean,\n\toptionsHeaders?: ProviderHeaders,\n\tdynamicHeaders?: Record<string, string>,\n\tsessionId?: string,\n): { client: Anthropic; isOAuthToken: boolean } {\n\t// Adaptive thinking models have interleaved thinking built in, so skip the beta header.\n\tconst needsInterleavedBeta = interleavedThinking && model.compat?.forceAdaptiveThinking !== true;\n\tconst betaFeatures: string[] = [];\n\tif (useFineGrainedToolStreamingBeta) {\n\t\tbetaFeatures.push(FINE_GRAINED_TOOL_STREAMING_BETA);\n\t}\n\tif (needsInterleavedBeta) {\n\t\tbetaFeatures.push(INTERLEAVED_THINKING_BETA);\n\t}\n\n\t// Copilot: Bearer auth, selective betas.\n\tif (model.provider === \"github-copilot\") {\n\t\tconst client = new Anthropic({\n\t\t\tapiKey: null,\n\t\t\tauthToken: apiKey ?? null,\n\t\t\tbaseURL: model.baseUrl,\n\t\t\tdangerouslyAllowBrowser: true,\n\t\t\tdefaultHeaders: mergeHeaders(\n\t\t\t\t{\n\t\t\t\t\taccept: \"application/json\",\n\t\t\t\t\t\"anthropic-dangerous-direct-browser-access\": \"true\",\n\t\t\t\t\t...(betaFeatures.length > 0 ? { \"anthropic-beta\": betaFeatures.join(\",\") } : {}),\n\t\t\t\t},\n\t\t\t\tmodel.headers,\n\t\t\t\tdynamicHeaders,\n\t\t\t\toptionsHeaders,\n\t\t\t),\n\t\t});\n\n\t\treturn { client, isOAuthToken: false };\n\t}\n\n\t// OAuth: Bearer auth, Claude Code identity headers\n\tif (apiKey && isOAuthToken(apiKey)) {\n\t\tconst client = new Anthropic({\n\t\t\tapiKey: null,\n\t\t\tauthToken: apiKey,\n\t\t\tbaseURL: model.baseUrl,\n\t\t\tdangerouslyAllowBrowser: true,\n\t\t\tdefaultHeaders: mergeHeaders(\n\t\t\t\t{\n\t\t\t\t\taccept: \"application/json\",\n\t\t\t\t\t\"anthropic-dangerous-direct-browser-access\": \"true\",\n\t\t\t\t\t\"anthropic-beta\": [\"claude-code-20250219\", \"oauth-2025-04-20\", ...betaFeatures].join(\",\"),\n\t\t\t\t\t\"user-agent\": `claude-cli/${claudeCodeVersion}`,\n\t\t\t\t\t\"x-app\": \"cli\",\n\t\t\t\t},\n\t\t\t\tmodel.headers,\n\t\t\t\toptionsHeaders,\n\t\t\t),\n\t\t});\n\n\t\treturn { client, isOAuthToken: true };\n\t}\n\n\t// API key or header-owned auth.\n\tconst sessionAffinityHeaders: ProviderHeaders =\n\t\tsessionId && getAnthropicCompat(model).sendSessionAffinityHeaders ? { \"x-session-affinity\": sessionId } : {};\n\tconst defaultHeaders = mergeHeaders(\n\t\t{\n\t\t\taccept: \"application/json\",\n\t\t\t\"anthropic-dangerous-direct-browser-access\": \"true\",\n\t\t\t...(betaFeatures.length > 0 ? { \"anthropic-beta\": betaFeatures.join(\",\") } : {}),\n\t\t},\n\t\tsessionAffinityHeaders,\n\t\tmodel.headers,\n\t\toptionsHeaders,\n\t);\n\tconst client = new Anthropic({\n\t\tapiKey: apiKey ?? null,\n\t\tauthToken: null,\n\t\tbaseURL: model.baseUrl,\n\t\tdangerouslyAllowBrowser: true,\n\t\tdefaultHeaders,\n\t});\n\n\treturn { client, isOAuthToken: false };\n}\n\nfunction buildParams(\n\tmodel: Model<\"anthropic-messages\">,\n\tcontext: Context,\n\tisOAuthToken: boolean,\n\toptions?: AnthropicOptions,\n): MessageCreateParamsStreaming {\n\tconst { cacheControl } = getCacheControl(model, options?.cacheRetention, options?.env);\n\tconst compat = getAnthropicCompat(model);\n\tconst transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);\n\tconst normalizeToolName = isOAuthToken ? toClaudeCodeName : (name: string) => name;\n\tconst toolPlacement = splitDeferredTools(\n\t\t{ ...context, messages: transformedMessages },\n\t\tcompat.supportsToolReferences,\n\t\tnormalizeToolName,\n\t);\n\tlet immediateTools = toolPlacement.immediate;\n\tlet deferredTools = [...toolPlacement.deferred.values()];\n\tif (immediateTools.length === 0 && deferredTools.length > 0) {\n\t\timmediateTools = deferredTools;\n\t\tdeferredTools = [];\n\t}\n\tconst deferredToolNames = new Set(deferredTools.map((tool) => normalizeToolName(tool.name)));\n\tconst params: MessageCreateParamsStreaming = {\n\t\tmodel: model.id,\n\t\tmessages: convertMessages(\n\t\t\ttransformedMessages,\n\t\t\tisOAuthToken,\n\t\t\tcacheControl,\n\t\t\tcompat.allowEmptySignature,\n\t\t\tdeferredToolNames,\n\t\t\tnormalizeToolName,\n\t\t),\n\t\tmax_tokens: options?.maxTokens ?? model.maxTokens,\n\t\tstream: true,\n\t};\n\n\t// For OAuth tokens, we MUST include Claude Code identity\n\tif (isOAuthToken) {\n\t\tparams.system = [\n\t\t\t{\n\t\t\t\ttype: \"text\",\n\t\t\t\ttext: \"You are Claude Code, Anthropic's official CLI for Claude.\",\n\t\t\t\t...(cacheControl ? { cache_control: cacheControl } : {}),\n\t\t\t},\n\t\t];\n\t\tif (context.systemPrompt) {\n\t\t\tparams.system.push({\n\t\t\t\ttype: \"text\",\n\t\t\t\ttext: sanitizeSurrogates(context.systemPrompt),\n\t\t\t\t...(cacheControl ? { cache_control: cacheControl } : {}),\n\t\t\t});\n\t\t}\n\t} else if (context.systemPrompt) {\n\t\t// Add cache control to system prompt for non-OAuth tokens\n\t\tparams.system = [\n\t\t\t{\n\t\t\t\ttype: \"text\",\n\t\t\t\ttext: sanitizeSurrogates(context.systemPrompt),\n\t\t\t\t...(cacheControl ? { cache_control: cacheControl } : {}),\n\t\t\t},\n\t\t];\n\t}\n\n\t// Temperature is incompatible with extended thinking and unsupported on Claude Opus 4.7+.\n\tif (options?.temperature !== undefined && !options?.thinkingEnabled && compat.supportsTemperature) {\n\t\tparams.temperature = options.temperature;\n\t}\n\n\tif (immediateTools.length > 0 || deferredTools.length > 0) {\n\t\tparams.tools = [\n\t\t\t...convertTools(\n\t\t\t\timmediateTools,\n\t\t\t\tisOAuthToken,\n\t\t\t\tcompat.supportsEagerToolInputStreaming,\n\t\t\t\tcompat.supportsCacheControlOnTools ? cacheControl : undefined,\n\t\t\t),\n\t\t\t...convertTools(deferredTools, isOAuthToken, compat.supportsEagerToolInputStreaming, undefined, true),\n\t\t];\n\t}\n\n\t// Configure thinking mode: adaptive, budget-based, or explicitly disabled.\n\tif (model.reasoning) {\n\t\tif (options?.thinkingEnabled) {\n\t\t\t// Default to \"summarized\" so Opus 4.7 and Mythos Preview behave like\n\t\t\t// older Claude 4 models (whose API default is also \"summarized\").\n\t\t\tconst display: AnthropicThinkingDisplay = options.thinkingDisplay ?? \"summarized\";\n\t\t\tif (model.compat?.forceAdaptiveThinking === true) {\n\t\t\t\t// Adaptive thinking: Claude decides when and how much to think.\n\t\t\t\tparams.thinking = { type: \"adaptive\", display };\n\t\t\t\tif (options.effort) {\n\t\t\t\t\t// The Anthropic SDK types can lag newly supported effort values such as \"xhigh\".\n\t\t\t\t\tparams.output_config =\n\t\t\t\t\t\toptions.effort === \"xhigh\"\n\t\t\t\t\t\t\t? ({ effort: options.effort } as unknown as NonNullable<\n\t\t\t\t\t\t\t\t\tMessageCreateParamsStreaming[\"output_config\"]\n\t\t\t\t\t\t\t\t>)\n\t\t\t\t\t\t\t: { effort: options.effort };\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Budget-based thinking for older models\n\t\t\t\tparams.thinking = {\n\t\t\t\t\ttype: \"enabled\",\n\t\t\t\t\tbudget_tokens: options.thinkingBudgetTokens || 1024,\n\t\t\t\t\tdisplay,\n\t\t\t\t};\n\t\t\t}\n\t\t} else if (options?.thinkingEnabled === false && model.thinkingLevelMap?.off !== null) {\n\t\t\tparams.thinking = { type: \"disabled\" };\n\t\t}\n\t}\n\n\tif (options?.metadata) {\n\t\tconst userId = options.metadata.user_id;\n\t\tif (typeof userId === \"string\") {\n\t\t\tparams.metadata = { user_id: userId };\n\t\t}\n\t}\n\n\tif (options?.toolChoice) {\n\t\tif (typeof options.toolChoice === \"string\") {\n\t\t\tparams.tool_choice = { type: options.toolChoice };\n\t\t} else {\n\t\t\tparams.tool_choice = options.toolChoice;\n\t\t}\n\t}\n\n\treturn params;\n}\n\n// Normalize tool call IDs to match Anthropic's required pattern and length\nfunction normalizeToolCallId(id: string): string {\n\treturn id.replace(/[^a-zA-Z0-9_-]/g, \"_\").slice(0, 64);\n}\n\nfunction convertToolResult(\n\tmsg: ToolResultMessage,\n\tisOAuthToken: boolean,\n\tdeferredToolNames: ReadonlySet<string>,\n\tloadedToolNames: Set<string>,\n\tnormalizeToolName: (name: string) => string,\n): { toolResult: ContentBlockParam; siblingContent: ContentBlockParam[] } {\n\tconst references: Array<{ type: \"tool_reference\"; tool_name: string }> = [];\n\tfor (const name of msg.addedToolNames ?? []) {\n\t\tconst normalizedName = normalizeToolName(name);\n\t\tif (!deferredToolNames.has(normalizedName) || loadedToolNames.has(normalizedName)) continue;\n\t\tloadedToolNames.add(normalizedName);\n\t\treferences.push({\n\t\t\ttype: \"tool_reference\",\n\t\t\ttool_name: isOAuthToken ? toClaudeCodeName(name) : name,\n\t\t});\n\t}\n\tconst convertedContent = convertContentBlocks(msg.content);\n\t// Anthropic rejects tool references mixed with ordinary tool-result content.\n\treturn {\n\t\ttoolResult: {\n\t\t\ttype: \"tool_result\",\n\t\t\ttool_use_id: msg.toolCallId,\n\t\t\tcontent: references.length > 0 ? references : convertedContent,\n\t\t\tis_error: msg.isError,\n\t\t},\n\t\tsiblingContent:\n\t\t\treferences.length === 0\n\t\t\t\t? []\n\t\t\t\t: typeof convertedContent === \"string\"\n\t\t\t\t\t? [{ type: \"text\", text: convertedContent }]\n\t\t\t\t\t: convertedContent,\n\t};\n}\n\nfunction convertMessages(\n\ttransformedMessages: Message[],\n\tisOAuthToken: boolean,\n\tcacheControl?: CacheControlEphemeral,\n\tallowEmptySignature = false,\n\tdeferredToolNames: ReadonlySet<string> = new Set(),\n\tnormalizeToolName: (name: string) => string = (name) => name,\n): MessageParam[] {\n\tconst params: MessageParam[] = [];\n\tconst loadedToolNames = new Set<string>();\n\n\tfor (let i = 0; i < transformedMessages.length; i++) {\n\t\tconst msg = transformedMessages[i];\n\n\t\tif (msg.role === \"user\") {\n\t\t\tif (typeof msg.content === \"string\") {\n\t\t\t\tif (msg.content.trim().length > 0) {\n\t\t\t\t\tparams.push({\n\t\t\t\t\t\trole: \"user\",\n\t\t\t\t\t\tcontent: sanitizeSurrogates(msg.content),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst blocks: ContentBlockParam[] = msg.content.map((item) => {\n\t\t\t\t\tif (item.type === \"text\") {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: sanitizeSurrogates(item.text),\n\t\t\t\t\t\t};\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\ttype: \"image\",\n\t\t\t\t\t\t\tsource: {\n\t\t\t\t\t\t\t\ttype: \"base64\",\n\t\t\t\t\t\t\t\tmedia_type: item.mimeType as \"image/jpeg\" | \"image/png\" | \"image/gif\" | \"image/webp\",\n\t\t\t\t\t\t\t\tdata: item.data,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tconst filteredBlocks = blocks.filter((b) => {\n\t\t\t\t\tif (b.type === \"text\") {\n\t\t\t\t\t\treturn b.text.trim().length > 0;\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t});\n\t\t\t\tif (filteredBlocks.length === 0) continue;\n\t\t\t\tparams.push({\n\t\t\t\t\trole: \"user\",\n\t\t\t\t\tcontent: filteredBlocks,\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (msg.role === \"assistant\") {\n\t\t\tconst blocks: ContentBlockParam[] = [];\n\n\t\t\tfor (const block of msg.content) {\n\t\t\t\tif (block.type === \"text\") {\n\t\t\t\t\tif (block.text.trim().length === 0) continue;\n\t\t\t\t\tblocks.push({\n\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\ttext: sanitizeSurrogates(block.text),\n\t\t\t\t\t});\n\t\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\t\t// Redacted thinking: pass the opaque payload back as redacted_thinking\n\t\t\t\t\tif (block.redacted) {\n\t\t\t\t\t\tblocks.push({\n\t\t\t\t\t\t\ttype: \"redacted_thinking\",\n\t\t\t\t\t\t\tdata: block.thinkingSignature!,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst thinkingSignature = block.thinkingSignature;\n\t\t\t\t\tconst hasThinkingSignature = !!thinkingSignature && thinkingSignature.trim().length > 0;\n\t\t\t\t\tif (block.thinking.trim().length === 0 && !hasThinkingSignature) continue;\n\t\t\t\t\t// If thinking signature is missing/empty (e.g., from aborted stream),\n\t\t\t\t\t// convert to plain text for Anthropic. Some compatible providers emit\n\t\t\t\t\t// and accept empty signatures, so let marked models preserve the block.\n\t\t\t\t\tif (!hasThinkingSignature) {\n\t\t\t\t\t\tblocks.push(\n\t\t\t\t\t\t\tallowEmptySignature\n\t\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\t\t\t\t\t\tthinking: sanitizeSurrogates(block.thinking),\n\t\t\t\t\t\t\t\t\t\tsignature: \"\",\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\ttext: sanitizeSurrogates(block.thinking),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tblocks.push({\n\t\t\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\t\t\tthinking: sanitizeSurrogates(block.thinking),\n\t\t\t\t\t\t\tsignature: thinkingSignature,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\t\tblocks.push({\n\t\t\t\t\t\ttype: \"tool_use\",\n\t\t\t\t\t\tid: block.id,\n\t\t\t\t\t\tname: isOAuthToken ? toClaudeCodeName(block.name) : block.name,\n\t\t\t\t\t\tinput: block.arguments ?? {},\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (blocks.length === 0) continue;\n\t\t\tparams.push({\n\t\t\t\trole: \"assistant\",\n\t\t\t\tcontent: blocks,\n\t\t\t});\n\t\t} else if (msg.role === \"toolResult\") {\n\t\t\t// Collect all consecutive toolResult messages, needed for z.ai Anthropic endpoint.\n\t\t\tconst toolResults: ContentBlockParam[] = [];\n\t\t\tconst siblingContent: ContentBlockParam[] = [];\n\t\t\tlet j = i;\n\t\t\twhile (j < transformedMessages.length && transformedMessages[j].role === \"toolResult\") {\n\t\t\t\tconst converted = convertToolResult(\n\t\t\t\t\ttransformedMessages[j] as ToolResultMessage,\n\t\t\t\t\tisOAuthToken,\n\t\t\t\t\tdeferredToolNames,\n\t\t\t\t\tloadedToolNames,\n\t\t\t\t\tnormalizeToolName,\n\t\t\t\t);\n\t\t\t\ttoolResults.push(converted.toolResult);\n\t\t\t\tsiblingContent.push(...converted.siblingContent);\n\t\t\t\tj++;\n\t\t\t}\n\n\t\t\t// Skip the messages we've already processed.\n\t\t\ti = j - 1;\n\n\t\t\t// Displaced reference-bearing results must follow every tool_result block.\n\t\t\tparams.push({\n\t\t\t\trole: \"user\",\n\t\t\t\tcontent: [...toolResults, ...siblingContent],\n\t\t\t});\n\t\t}\n\t}\n\n\t// Add cache_control to the last user message to cache conversation history\n\tif (cacheControl && params.length > 0) {\n\t\tconst lastMessage = params[params.length - 1];\n\t\tif (lastMessage.role === \"user\") {\n\t\t\tif (Array.isArray(lastMessage.content)) {\n\t\t\t\tconst lastBlock = lastMessage.content[lastMessage.content.length - 1];\n\t\t\t\tif (\n\t\t\t\t\tlastBlock &&\n\t\t\t\t\t(lastBlock.type === \"text\" || lastBlock.type === \"image\" || lastBlock.type === \"tool_result\")\n\t\t\t\t) {\n\t\t\t\t\t(lastBlock as any).cache_control = cacheControl;\n\t\t\t\t}\n\t\t\t} else if (typeof lastMessage.content === \"string\") {\n\t\t\t\tlastMessage.content = [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\ttext: lastMessage.content,\n\t\t\t\t\t\tcache_control: cacheControl,\n\t\t\t\t\t},\n\t\t\t\t] as any;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn params;\n}\n\nfunction shouldUseFineGrainedToolStreamingBeta(model: Model<\"anthropic-messages\">, context: Context): boolean {\n\treturn !!context.tools?.length && !getAnthropicCompat(model).supportsEagerToolInputStreaming;\n}\n\nfunction convertTools(\n\ttools: Tool[],\n\tisOAuthToken: boolean,\n\tsupportsEagerToolInputStreaming: boolean,\n\tcacheControl?: CacheControlEphemeral,\n\tdeferLoading = false,\n): Anthropic.Messages.Tool[] {\n\tif (!tools) return [];\n\n\treturn tools.map((tool, index) => {\n\t\tconst schema = tool.parameters as { properties?: unknown; required?: string[] };\n\n\t\treturn {\n\t\t\tname: isOAuthToken ? toClaudeCodeName(tool.name) : tool.name,\n\t\t\tdescription: tool.description,\n\t\t\t...(supportsEagerToolInputStreaming ? { eager_input_streaming: true } : {}),\n\t\t\tinput_schema: {\n\t\t\t\ttype: \"object\",\n\t\t\t\tproperties: schema.properties ?? {},\n\t\t\t\trequired: schema.required ?? [],\n\t\t\t},\n\t\t\t...(deferLoading ? { defer_loading: true } : {}),\n\t\t\t...(cacheControl && index === tools.length - 1 ? { cache_control: cacheControl } : {}),\n\t\t};\n\t});\n}\n\nfunction mapStopReason(\n\treason: Anthropic.Messages.StopReason | string,\n\tstopDetails?: RefusalStopDetails | null,\n): { stopReason: StopReason; errorMessage?: string } {\n\tswitch (reason) {\n\t\tcase \"end_turn\":\n\t\t\treturn { stopReason: \"stop\" };\n\t\tcase \"max_tokens\":\n\t\t\treturn { stopReason: \"length\" };\n\t\tcase \"tool_use\":\n\t\t\treturn { stopReason: \"toolUse\" };\n\t\tcase \"refusal\":\n\t\t\treturn {\n\t\t\t\tstopReason: \"error\",\n\t\t\t\terrorMessage: stopDetails?.explanation || `The model refused to complete the request`,\n\t\t\t};\n\t\tcase \"pause_turn\": // Stop is good enough -> resubmit\n\t\t\treturn { stopReason: \"stop\" };\n\t\tcase \"stop_sequence\":\n\t\t\treturn { stopReason: \"stop\" }; // We don't supply stop sequences, so this should never happen\n\t\tcase \"sensitive\": // Content flagged by safety filters (not yet in SDK types)\n\t\t\treturn { stopReason: \"error\" };\n\t\tdefault:\n\t\t\t// Handle unknown stop reasons gracefully (API may add new values)\n\t\t\tthrow new Error(`Unhandled stop reason: ${reason}`);\n\t}\n}\n"]}
|