@code-yeongyu/senpi-ai 2026.10.1 → 2026.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/api/anthropic-messages.js +153 -29
- package/dist/api/anthropic-tool-references.d.ts +14 -0
- package/dist/api/anthropic-tool-references.js +107 -0
- package/dist/api/constrained-sampling.d.ts +8 -2
- package/dist/api/constrained-sampling.js +19 -8
- package/dist/api/openai-responses-shared.js +7 -5
- package/dist/api-registry.d.ts +5 -1
- package/dist/api-registry.js +16 -6
- package/dist/auth/oauth/anthropic.js +52 -1
- package/dist/compat.js +4 -1
- package/dist/env-api-keys.d.ts +5 -0
- package/dist/env-api-keys.js +5 -0
- package/dist/providers/anthropic.js +24 -2
- package/dist/providers/data/.manifest.json +1 -1
- package/dist/providers/data/alibaba-token-plan.json +1 -1
- package/dist/providers/data/amazon-bedrock.json +1 -1
- package/dist/providers/data/cloudflare-ai-gateway.json +1 -1
- package/dist/providers/data/cloudflare-workers-ai.json +1 -1
- package/dist/providers/data/fireworks.json +1 -1
- package/dist/providers/data/moonshotai-cn.json +1 -1
- package/dist/providers/data/moonshotai.json +1 -1
- package/dist/providers/data/nvidia.json +1 -1
- package/dist/providers/data/opencode.json +1 -1
- package/dist/providers/data/opengateway.json +1 -1
- package/dist/providers/data/openrouter.json +1 -1
- package/dist/providers/data/qwen-token-plan-cn.json +1 -1
- package/dist/providers/data/qwen-token-plan-individual.json +1 -1
- package/dist/providers/data/qwen-token-plan.json +1 -1
- package/dist/providers/data/radius.json +1 -1
- package/dist/providers/data/together.json +1 -1
- package/dist/providers/data/venice.json +1 -1
- package/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/dist/providers/data/zai-coding-cn.json +1 -1
- package/dist/providers/data/zai.json +1 -1
- package/dist/providers/opengateway-catalog.d.ts +43 -0
- package/dist/providers/opengateway-catalog.js +151 -0
- package/dist/providers/opengateway-refresh.d.ts +20 -0
- package/dist/providers/opengateway-refresh.js +129 -0
- package/dist/providers/opengateway.js +13 -2
- package/dist/utils/oauth-page.js +1 -1
- package/dist/utils/openai-input-cap.d.ts +4 -0
- package/dist/utils/openai-input-cap.js +18 -0
- package/dist/utils/overflow.js +2 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -1626,10 +1626,10 @@ Browser compatibility notes:
|
|
|
1626
1626
|
|
|
1627
1627
|
## Bundling and Tree Shaking
|
|
1628
1628
|
|
|
1629
|
-
For small bundles, import only the providers you need:
|
|
1629
|
+
For small bundles and low-overhead unbundled scripts, import the model runtime and only the providers you need:
|
|
1630
1630
|
|
|
1631
1631
|
```typescript
|
|
1632
|
-
import { createModels } from '@earendil-works/pi-ai';
|
|
1632
|
+
import { createModels } from '@earendil-works/pi-ai/models';
|
|
1633
1633
|
import { openaiProvider } from '@earendil-works/pi-ai/providers/openai';
|
|
1634
1634
|
|
|
1635
1635
|
const models = createModels();
|
|
@@ -1638,7 +1638,8 @@ models.setProvider(openaiProvider());
|
|
|
1638
1638
|
|
|
1639
1639
|
Rules:
|
|
1640
1640
|
|
|
1641
|
-
- `@earendil-works/pi-ai`
|
|
1641
|
+
- `@earendil-works/pi-ai/models` exports the model runtime (`createModels`, `createProvider`, model helpers, and their types) without TypeBox, built-in catalogs, or SDK implementations. Other types can still use `import type` from the root.
|
|
1642
|
+
- `@earendil-works/pi-ai` is the core entrypoint and does not import built-in catalogs, real provider factories, or SDK implementations, but it eagerly imports TypeBox and schema validation. Unbundled Node scripts do not tree-shake its unused exports; prefer `./models`, `./providers/faux`, and specific `./utils/*` subpaths when those are all you need.
|
|
1642
1643
|
- `@earendil-works/pi-ai/providers/<provider>` imports that provider's catalog and lazy API wrapper only.
|
|
1643
1644
|
- `@earendil-works/pi-ai/providers/all` imports every built-in provider factory and all catalogs. Use it only when you want the full built-in set.
|
|
1644
1645
|
- With code splitting, provider SDKs stay in lazy chunks and load on first request.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
import { ANTHROPIC_FEDERATION_RULE_ID_ENV, ANTHROPIC_IDENTITY_TOKEN_FILE_ENV, ANTHROPIC_ORGANIZATION_ID_ENV, ANTHROPIC_SERVICE_ACCOUNT_ID_ENV, ANTHROPIC_WORKSPACE_ID_ENV, } from "../env-api-keys.js";
|
|
2
3
|
import { calculateCost } from "../models.js";
|
|
3
4
|
import { readProviderDiagnostic } from "../provider-diagnostic.js";
|
|
4
5
|
import { registerSessionResourceCleanup } from "../session-resources.js";
|
|
@@ -27,9 +28,9 @@ import { sendWithForcedToolChoiceFallback } from "../utils/tool-choice-fallback.
|
|
|
27
28
|
import { resolveRootObjectSchema } from "../utils/tool-schema-compat.js";
|
|
28
29
|
import { getCurrentTools, getDeclaredTools, getInitialSystemMessage, hasToolRedefinitions, normalizeContext, resolveTranscript, } from "../utils/transcript.js";
|
|
29
30
|
import { sanitizeAnthropicToolPairs } from "./anthropic-tool-pairs.js";
|
|
30
|
-
import { demoteUnavailableToolReferences } from "./anthropic-tool-references.js";
|
|
31
|
+
import { demoteToolReferenceReplay, demoteUnavailableToolReferences } from "./anthropic-tool-references.js";
|
|
31
32
|
import { resolveCloudflareBaseUrl } from "./cloudflare.js";
|
|
32
|
-
import { getJsonSchemaToolParameters, resolveJsonSchemaStrictSampling } from "./constrained-sampling.js";
|
|
33
|
+
import { getJsonSchemaToolParameters, resolveJsonSchemaStrictSampling, } from "./constrained-sampling.js";
|
|
33
34
|
import { withGitHubCopilotFailureNote } from "./github-copilot-errors.js";
|
|
34
35
|
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
|
|
35
36
|
import { ANTHROPIC_RESERVED_BODY_KEYS, adjustMaxTokensForThinking, buildBaseOptions, clampMaxTokensToContext, } from "./simple-options.js";
|
|
@@ -70,7 +71,7 @@ function getCacheControl(model, cacheRetention, env) {
|
|
|
70
71
|
// The bundled Claude Code version and the floor of the advertised `claude-cli/<version>`
|
|
71
72
|
// (see utils/claude-code-version.ts). Keep this exact declaration: a downstream installer
|
|
72
73
|
// (oh-my-openagent) rewrites it byte-for-byte in the installed dist and bundle.
|
|
73
|
-
const claudeCodeVersion = "2.1.
|
|
74
|
+
const claudeCodeVersion = "2.1.286";
|
|
74
75
|
// Claude Code 2.x tool names (canonical casing)
|
|
75
76
|
// Source: https://cchistory.mariozechner.at/data/prompts-2.1.11.md
|
|
76
77
|
// To update: https://github.com/badlogic/cchistory
|
|
@@ -231,18 +232,23 @@ function supportsMidConvoToolChanges(model) {
|
|
|
231
232
|
// A provider can reject its own empty signatures. Learn that capability for one
|
|
232
233
|
// conversation without changing the shared model definition used by other sessions.
|
|
233
234
|
const unsignedThinkingTextReplayFallbacks = new Set();
|
|
235
|
+
// Endpoints that rejected a replayed tool_reference for one conversation (senpi #2568).
|
|
236
|
+
const toolReferenceReplayFallbacks = new Set();
|
|
234
237
|
registerSessionResourceCleanup((sessionId) => {
|
|
235
238
|
// One small entry per (session, base URL, model) that ever hit the fallback;
|
|
236
239
|
// drop the session's entries at teardown so long-lived hosts do not collect
|
|
237
240
|
// them for every session that ever ran.
|
|
238
241
|
if (sessionId === undefined) {
|
|
239
242
|
unsignedThinkingTextReplayFallbacks.clear();
|
|
243
|
+
toolReferenceReplayFallbacks.clear();
|
|
240
244
|
return;
|
|
241
245
|
}
|
|
242
246
|
const prefix = `${sessionId}\u0000`;
|
|
243
|
-
for (const
|
|
244
|
-
|
|
245
|
-
|
|
247
|
+
for (const fallbacks of [unsignedThinkingTextReplayFallbacks, toolReferenceReplayFallbacks]) {
|
|
248
|
+
for (const key of fallbacks) {
|
|
249
|
+
if (key.startsWith(prefix))
|
|
250
|
+
fallbacks.delete(key);
|
|
251
|
+
}
|
|
246
252
|
}
|
|
247
253
|
});
|
|
248
254
|
function unsignedThinkingFallbackKey(model, sessionId) {
|
|
@@ -256,6 +262,12 @@ function isInvalidUnsignedThinkingSignatureError(error) {
|
|
|
256
262
|
error instanceof Error &&
|
|
257
263
|
/Invalid signature in thinking block/i.test(error.message));
|
|
258
264
|
}
|
|
265
|
+
function isToolReferenceNotFoundError(error) {
|
|
266
|
+
return (error instanceof Error &&
|
|
267
|
+
"status" in error &&
|
|
268
|
+
error.status === 400 &&
|
|
269
|
+
/Tool reference '[^']*' not found in available tools/i.test(error.message));
|
|
270
|
+
}
|
|
259
271
|
function mergeHeaders(...headerSources) {
|
|
260
272
|
const merged = {};
|
|
261
273
|
for (const headers of headerSources) {
|
|
@@ -279,16 +291,58 @@ function hasHeader(headers, name) {
|
|
|
279
291
|
}
|
|
280
292
|
return false;
|
|
281
293
|
}
|
|
282
|
-
function
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (hasHeader(headers, "authorization") ||
|
|
294
|
+
function hasRequestAuth(apiKey, headers) {
|
|
295
|
+
return (!!apiKey ||
|
|
296
|
+
hasHeader(headers, "authorization") ||
|
|
286
297
|
hasHeader(headers, "x-api-key") ||
|
|
287
|
-
hasHeader(headers, "cf-aig-authorization"))
|
|
288
|
-
|
|
298
|
+
hasHeader(headers, "cf-aig-authorization"));
|
|
299
|
+
}
|
|
300
|
+
function assertRequestAuth(provider, apiKey, headers) {
|
|
301
|
+
if (!hasRequestAuth(apiKey, headers))
|
|
302
|
+
throw new Error(`No API key for provider: ${provider}`);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Anthropic SDK client that never runs the SDK's own credential chain
|
|
306
|
+
* (ANTHROPIC_PROFILE config files, federation env vars). Without this, every
|
|
307
|
+
* client built with `apiKey: null, authToken: null` for header-owned auth would
|
|
308
|
+
* also resolve and exchange SDK credentials behind pi's auth resolver.
|
|
309
|
+
*/
|
|
310
|
+
class PiAnthropic extends Anthropic {
|
|
311
|
+
_shouldResolveDefaultCredentials() {
|
|
312
|
+
return false;
|
|
289
313
|
}
|
|
290
|
-
throw new Error(`No API key for provider: ${provider}`);
|
|
291
314
|
}
|
|
315
|
+
/**
|
|
316
|
+
* Workload identity federation config from the ANTHROPIC_* variables the
|
|
317
|
+
* Anthropic SDK documents; the SDK performs the token exchange and refresh.
|
|
318
|
+
* Only for the anthropic provider, since the exchange is an Anthropic API
|
|
319
|
+
* endpoint, and only when no key or auth header was resolved.
|
|
320
|
+
*/
|
|
321
|
+
function getAnthropicFederation(model, apiKey, headers, env) {
|
|
322
|
+
if (model.provider !== "anthropic" || hasRequestAuth(apiKey, headers))
|
|
323
|
+
return undefined;
|
|
324
|
+
const federationRuleId = getProviderEnvValue(ANTHROPIC_FEDERATION_RULE_ID_ENV, env);
|
|
325
|
+
const organizationId = getProviderEnvValue(ANTHROPIC_ORGANIZATION_ID_ENV, env);
|
|
326
|
+
const identityTokenFile = getProviderEnvValue(ANTHROPIC_IDENTITY_TOKEN_FILE_ENV, env);
|
|
327
|
+
if (!federationRuleId || !organizationId || !identityTokenFile)
|
|
328
|
+
return undefined;
|
|
329
|
+
return {
|
|
330
|
+
organization_id: organizationId,
|
|
331
|
+
workspace_id: getProviderEnvValue(ANTHROPIC_WORKSPACE_ID_ENV, env),
|
|
332
|
+
authentication: {
|
|
333
|
+
type: "oidc_federation",
|
|
334
|
+
federation_rule_id: federationRuleId,
|
|
335
|
+
service_account_id: getProviderEnvValue(ANTHROPIC_SERVICE_ACCOUNT_ID_ENV, env),
|
|
336
|
+
identity_token: { source: "file", path: identityTokenFile },
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* The SDK caches the federated access token per client, but pi creates a client
|
|
342
|
+
* per request. Keep one client for the current federation config and fetch, and
|
|
343
|
+
* clone it per request with `withOptions()`, which shares the token cache.
|
|
344
|
+
*/
|
|
345
|
+
let federationClient;
|
|
292
346
|
function isRecord(value) {
|
|
293
347
|
return typeof value === "object" && value !== null;
|
|
294
348
|
}
|
|
@@ -904,7 +958,9 @@ export const stream = (model, context, options) => {
|
|
|
904
958
|
else {
|
|
905
959
|
const apiKey = options?.apiKey;
|
|
906
960
|
const optionsHeaders = providerHeadersToRecord(options?.headers);
|
|
907
|
-
|
|
961
|
+
const federation = getAnthropicFederation(model, apiKey, optionsHeaders, options?.env);
|
|
962
|
+
if (!federation)
|
|
963
|
+
assertRequestAuth(model.provider, apiKey, optionsHeaders);
|
|
908
964
|
let copilotDynamicHeaders;
|
|
909
965
|
if (model.provider === "github-copilot") {
|
|
910
966
|
const hasImages = hasCopilotVisionInput(normalizedContext.messages);
|
|
@@ -916,7 +972,7 @@ export const stream = (model, context, options) => {
|
|
|
916
972
|
const cacheRetention = resolveCacheRetention(options?.cacheRetention ?? model.cacheRetention, options?.env, "short");
|
|
917
973
|
const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
|
|
918
974
|
openClient = () => {
|
|
919
|
-
const created = createClient(model, apiKey, options?.interleavedThinking ?? true, shouldUseFineGrainedToolStreamingBeta(model, normalizedContext), options?.refusalFallbacks !== undefined, optionsHeaders, options?.fetch, copilotDynamicHeaders, cacheSessionId, options?.env);
|
|
975
|
+
const created = createClient(model, apiKey, options?.interleavedThinking ?? true, shouldUseFineGrainedToolStreamingBeta(model, normalizedContext), options?.refusalFallbacks !== undefined, optionsHeaders, options?.fetch, copilotDynamicHeaders, cacheSessionId, options?.env, federation);
|
|
920
976
|
client = created.client;
|
|
921
977
|
isOAuth = created.isOAuthToken;
|
|
922
978
|
advertisedClaudeCodeVersion = created.claudeCodeVersion;
|
|
@@ -949,6 +1005,8 @@ export const stream = (model, context, options) => {
|
|
|
949
1005
|
let unsignedThinkingReplay = fallbackKey && unsignedThinkingTextReplayFallbacks.has(fallbackKey)
|
|
950
1006
|
? "text"
|
|
951
1007
|
: getAnthropicCompat(model).unsignedThinkingReplay;
|
|
1008
|
+
let demoteReferenceReplay = fallbackKey !== undefined && toolReferenceReplayFallbacks.has(fallbackKey);
|
|
1009
|
+
let requestReplaysReferences = false;
|
|
952
1010
|
const createRequest = async () => {
|
|
953
1011
|
let params = buildParams(model, normalizedContext, isOAuth, options, unsignedThinkingReplay);
|
|
954
1012
|
const nextParams = await options?.onPayload?.(params, model);
|
|
@@ -957,7 +1015,12 @@ export const stream = (model, context, options) => {
|
|
|
957
1015
|
}
|
|
958
1016
|
params = sanitizeAdaptiveThinkingPayload(model, params, options);
|
|
959
1017
|
params = sanitizeUnsupportedNativeTools(model, params);
|
|
960
|
-
params =
|
|
1018
|
+
params = demoteUnavailableToolReferences(params);
|
|
1019
|
+
const withoutReferenceReplay = demoteToolReferenceReplay(params);
|
|
1020
|
+
requestReplaysReferences = withoutReferenceReplay !== params;
|
|
1021
|
+
if (demoteReferenceReplay)
|
|
1022
|
+
params = withoutReferenceReplay;
|
|
1023
|
+
params = sanitizeAnthropicToolPairs(params);
|
|
961
1024
|
const payloadRequestMetadata = extractPayloadRequestMetadata(params);
|
|
962
1025
|
params = payloadRequestMetadata.params;
|
|
963
1026
|
const limitedTools = limitGitHubCopilotTools(model.provider, params.tools, params.tool_choice);
|
|
@@ -994,6 +1057,12 @@ export const stream = (model, context, options) => {
|
|
|
994
1057
|
unsignedThinkingTextReplayFallbacks.add(fallbackKey);
|
|
995
1058
|
return createRequest();
|
|
996
1059
|
}
|
|
1060
|
+
if (!demoteReferenceReplay && requestReplaysReferences && isToolReferenceNotFoundError(error)) {
|
|
1061
|
+
demoteReferenceReplay = true;
|
|
1062
|
+
if (fallbackKey)
|
|
1063
|
+
toolReferenceReplayFallbacks.add(fallbackKey);
|
|
1064
|
+
return createRequest();
|
|
1065
|
+
}
|
|
997
1066
|
if (await retryWithNewerClaudeCode(error)) {
|
|
998
1067
|
try {
|
|
999
1068
|
return await createRequest();
|
|
@@ -1437,7 +1506,10 @@ function mapThinkingLevelToEffort(model, level) {
|
|
|
1437
1506
|
}
|
|
1438
1507
|
export const streamSimple = (model, context, options) => {
|
|
1439
1508
|
const apiKey = options?.apiKey;
|
|
1440
|
-
|
|
1509
|
+
const optionsHeaders = providerHeadersToRecord(options?.headers);
|
|
1510
|
+
if (!getAnthropicFederation(model, apiKey, optionsHeaders, options?.env)) {
|
|
1511
|
+
assertRequestAuth(model.provider, apiKey, optionsHeaders);
|
|
1512
|
+
}
|
|
1441
1513
|
const base = {
|
|
1442
1514
|
...buildBaseOptions(model, context, options, options?.apiKey),
|
|
1443
1515
|
toolChoice: options?.toolChoice,
|
|
@@ -1472,7 +1544,7 @@ export const streamSimple = (model, context, options) => {
|
|
|
1472
1544
|
function isOAuthToken(apiKey) {
|
|
1473
1545
|
return apiKey.includes("sk-ant-oat");
|
|
1474
1546
|
}
|
|
1475
|
-
function createClient(model, apiKey, interleavedThinking, useFineGrainedToolStreamingBeta, useServerSideFallbackBeta, optionsHeaders, fetch, dynamicHeaders, sessionId, env) {
|
|
1547
|
+
function createClient(model, apiKey, interleavedThinking, useFineGrainedToolStreamingBeta, useServerSideFallbackBeta, optionsHeaders, fetch, dynamicHeaders, sessionId, env, federation) {
|
|
1476
1548
|
// Adaptive thinking models have interleaved thinking built in, so skip the beta header.
|
|
1477
1549
|
const needsInterleavedBeta = interleavedThinking && !supportsAdaptiveThinking(model);
|
|
1478
1550
|
const betaFeatures = [];
|
|
@@ -1486,7 +1558,7 @@ function createClient(model, apiKey, interleavedThinking, useFineGrainedToolStre
|
|
|
1486
1558
|
betaFeatures.push(SERVER_SIDE_FALLBACK_BETA);
|
|
1487
1559
|
}
|
|
1488
1560
|
if (model.provider === "cloudflare-ai-gateway") {
|
|
1489
|
-
const client = new
|
|
1561
|
+
const client = new PiAnthropic({
|
|
1490
1562
|
apiKey: null,
|
|
1491
1563
|
authToken: null,
|
|
1492
1564
|
baseURL: resolveCloudflareBaseUrl(model, env),
|
|
@@ -1505,7 +1577,7 @@ function createClient(model, apiKey, interleavedThinking, useFineGrainedToolStre
|
|
|
1505
1577
|
}
|
|
1506
1578
|
// Copilot: Bearer auth, selective betas.
|
|
1507
1579
|
if (model.provider === "github-copilot") {
|
|
1508
|
-
const client = new
|
|
1580
|
+
const client = new PiAnthropic({
|
|
1509
1581
|
apiKey: null,
|
|
1510
1582
|
authToken: apiKey,
|
|
1511
1583
|
baseURL: model.baseUrl,
|
|
@@ -1522,7 +1594,7 @@ function createClient(model, apiKey, interleavedThinking, useFineGrainedToolStre
|
|
|
1522
1594
|
// OAuth: Bearer auth, Claude Code identity headers
|
|
1523
1595
|
if (apiKey && isOAuthToken(apiKey)) {
|
|
1524
1596
|
const advertisedClaudeCodeVersion = getClaudeCodeVersion(claudeCodeVersion, env);
|
|
1525
|
-
const client = new
|
|
1597
|
+
const client = new PiAnthropic({
|
|
1526
1598
|
apiKey: null,
|
|
1527
1599
|
authToken: apiKey,
|
|
1528
1600
|
baseURL: model.baseUrl,
|
|
@@ -1538,7 +1610,7 @@ function createClient(model, apiKey, interleavedThinking, useFineGrainedToolStre
|
|
|
1538
1610
|
});
|
|
1539
1611
|
return { client, isOAuthToken: true, claudeCodeVersion: advertisedClaudeCodeVersion };
|
|
1540
1612
|
}
|
|
1541
|
-
// API key auth
|
|
1613
|
+
// API key, header-owned auth, or workload identity federation.
|
|
1542
1614
|
const affinityCompat = getAnthropicCompat(model);
|
|
1543
1615
|
const sessionAffinityHeaders = {};
|
|
1544
1616
|
if (sessionId && affinityCompat.sendSessionAffinityHeaders) {
|
|
@@ -1547,17 +1619,33 @@ function createClient(model, apiKey, interleavedThinking, useFineGrainedToolStre
|
|
|
1547
1619
|
const header = affinityCompat.sessionAffinityFormat === "openrouter" ? "x-session-id" : "x-session-affinity";
|
|
1548
1620
|
sessionAffinityHeaders[header] = sessionId;
|
|
1549
1621
|
}
|
|
1550
|
-
const
|
|
1622
|
+
const defaultHeaders = sanitizeAdaptiveThinkingHeaders(model, mergeClientHeaders(model, {
|
|
1623
|
+
accept: "application/json",
|
|
1624
|
+
"anthropic-dangerous-direct-browser-access": "true",
|
|
1625
|
+
...(betaFeatures.length > 0 ? { "anthropic-beta": betaFeatures.join(",") } : {}),
|
|
1626
|
+
}, sessionAffinityHeaders, model.headers, optionsHeaders));
|
|
1627
|
+
if (federation) {
|
|
1628
|
+
const key = JSON.stringify([model.baseUrl, federation]);
|
|
1629
|
+
if (federationClient?.key !== key || federationClient.fetch !== fetch) {
|
|
1630
|
+
const client = new PiAnthropic({
|
|
1631
|
+
apiKey: null,
|
|
1632
|
+
authToken: null,
|
|
1633
|
+
config: federation,
|
|
1634
|
+
baseURL: model.baseUrl,
|
|
1635
|
+
dangerouslyAllowBrowser: true,
|
|
1636
|
+
fetch,
|
|
1637
|
+
});
|
|
1638
|
+
federationClient = { key, fetch, client };
|
|
1639
|
+
}
|
|
1640
|
+
return { client: federationClient.client.withOptions({ defaultHeaders }), isOAuthToken: false };
|
|
1641
|
+
}
|
|
1642
|
+
const client = new PiAnthropic({
|
|
1551
1643
|
apiKey: apiKey ?? null,
|
|
1552
1644
|
authToken: null,
|
|
1553
1645
|
baseURL: model.baseUrl,
|
|
1554
1646
|
dangerouslyAllowBrowser: true,
|
|
1555
1647
|
fetch,
|
|
1556
|
-
defaultHeaders
|
|
1557
|
-
accept: "application/json",
|
|
1558
|
-
"anthropic-dangerous-direct-browser-access": "true",
|
|
1559
|
-
...(betaFeatures.length > 0 ? { "anthropic-beta": betaFeatures.join(",") } : {}),
|
|
1560
|
-
}, sessionAffinityHeaders, model.headers, optionsHeaders)),
|
|
1648
|
+
defaultHeaders,
|
|
1561
1649
|
});
|
|
1562
1650
|
return { client, isOAuthToken: false };
|
|
1563
1651
|
}
|
|
@@ -2142,11 +2230,47 @@ function insertThinkingLevelMessages(converted, activeEffort) {
|
|
|
2142
2230
|
function shouldUseFineGrainedToolStreamingBeta(model, context) {
|
|
2143
2231
|
return getCurrentTools(context.messages).length > 0 && !getAnthropicCompat(model).supportsEagerToolInputStreaming;
|
|
2144
2232
|
}
|
|
2233
|
+
// Keywords Anthropic strict tool use rejects with a 400 for the whole request.
|
|
2234
|
+
// https://platform.claude.com/docs/en/build-with-claude/structured-outputs#json-schema-limitations
|
|
2235
|
+
const ANTHROPIC_STRICT_UNSUPPORTED_KEYWORDS = new Set([
|
|
2236
|
+
"minimum",
|
|
2237
|
+
"maximum",
|
|
2238
|
+
"exclusiveMinimum",
|
|
2239
|
+
"exclusiveMaximum",
|
|
2240
|
+
"multipleOf",
|
|
2241
|
+
"maxItems",
|
|
2242
|
+
"uniqueItems",
|
|
2243
|
+
"minContains",
|
|
2244
|
+
"maxContains",
|
|
2245
|
+
"minProperties",
|
|
2246
|
+
"maxProperties",
|
|
2247
|
+
]);
|
|
2248
|
+
const ANTHROPIC_STRICT_STRING_FORMATS = new Set([
|
|
2249
|
+
"date-time",
|
|
2250
|
+
"time",
|
|
2251
|
+
"date",
|
|
2252
|
+
"duration",
|
|
2253
|
+
"email",
|
|
2254
|
+
"hostname",
|
|
2255
|
+
"uri",
|
|
2256
|
+
"ipv4",
|
|
2257
|
+
"ipv6",
|
|
2258
|
+
"uuid",
|
|
2259
|
+
]);
|
|
2260
|
+
const isAnthropicStrictUnsupportedKeyword = (key, value) => {
|
|
2261
|
+
if (ANTHROPIC_STRICT_UNSUPPORTED_KEYWORDS.has(key))
|
|
2262
|
+
return true;
|
|
2263
|
+
if (key === "minItems")
|
|
2264
|
+
return value !== 0 && value !== 1;
|
|
2265
|
+
if (key === "format")
|
|
2266
|
+
return typeof value !== "string" || !ANTHROPIC_STRICT_STRING_FORMATS.has(value);
|
|
2267
|
+
return false;
|
|
2268
|
+
};
|
|
2145
2269
|
function convertTools(tools, isOAuthToken, supportsEagerToolInputStreaming, supportsStrictTools, cacheControl, deferLoading = false) {
|
|
2146
2270
|
if (!tools)
|
|
2147
2271
|
return [];
|
|
2148
2272
|
return tools.map((tool, index) => {
|
|
2149
|
-
const strict = resolveJsonSchemaStrictSampling(tool, supportsStrictTools);
|
|
2273
|
+
const strict = resolveJsonSchemaStrictSampling(tool, supportsStrictTools, isAnthropicStrictUnsupportedKeyword);
|
|
2150
2274
|
const parameters = getJsonSchemaToolParameters(tool, strict);
|
|
2151
2275
|
// A root union carries no top-level properties, so reading them directly
|
|
2152
2276
|
// would advertise the tool to the model as taking no arguments at all.
|
|
@@ -1,3 +1,17 @@
|
|
|
1
1
|
import type { MessageCreateParamsStreaming } from "@anthropic-ai/sdk/resources/beta/messages/messages.js";
|
|
2
2
|
export declare function demoteUnavailableToolReferences(params: MessageCreateParamsStreaming): MessageCreateParamsStreaming;
|
|
3
|
+
/**
|
|
4
|
+
* Some Anthropic-compatible endpoints run a native tool search inside one request
|
|
5
|
+
* but reject every `tool_reference` replayed from history once the request
|
|
6
|
+
* carries more than a handful of tools: "Tool reference '<name>' not found in
|
|
7
|
+
* available tools" even though `tools` defines the name (senpi #2568). The
|
|
8
|
+
* request is then retried with the replay demoted to text: a native search pair
|
|
9
|
+
* (`server_tool_use` plus its `tool_search_tool_result`, which may sit in a later
|
|
10
|
+
* assistant message) becomes a note naming the tools it found, and
|
|
11
|
+
* `tool_reference` items inside a client `tool_result` become one text item.
|
|
12
|
+
* A deferred tool is loaded only by a replayed reference, so every tool the demoted
|
|
13
|
+
* references named is sent without `defer_loading`: it stays callable without another
|
|
14
|
+
* search. Returns `params` unchanged when the history replays no reference.
|
|
15
|
+
*/
|
|
16
|
+
export declare function demoteToolReferenceReplay(params: MessageCreateParamsStreaming): MessageCreateParamsStreaming;
|
|
3
17
|
//# sourceMappingURL=anthropic-tool-references.d.ts.map
|
|
@@ -183,6 +183,113 @@ export function demoteUnavailableToolReferences(params) {
|
|
|
183
183
|
return params;
|
|
184
184
|
return { ...params, messages: rewrittenMessages };
|
|
185
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Some Anthropic-compatible endpoints run a native tool search inside one request
|
|
188
|
+
* but reject every `tool_reference` replayed from history once the request
|
|
189
|
+
* carries more than a handful of tools: "Tool reference '<name>' not found in
|
|
190
|
+
* available tools" even though `tools` defines the name (senpi #2568). The
|
|
191
|
+
* request is then retried with the replay demoted to text: a native search pair
|
|
192
|
+
* (`server_tool_use` plus its `tool_search_tool_result`, which may sit in a later
|
|
193
|
+
* assistant message) becomes a note naming the tools it found, and
|
|
194
|
+
* `tool_reference` items inside a client `tool_result` become one text item.
|
|
195
|
+
* A deferred tool is loaded only by a replayed reference, so every tool the demoted
|
|
196
|
+
* references named is sent without `defer_loading`: it stays callable without another
|
|
197
|
+
* search. Returns `params` unchanged when the history replays no reference.
|
|
198
|
+
*/
|
|
199
|
+
export function demoteToolReferenceReplay(params) {
|
|
200
|
+
const messages = params.messages;
|
|
201
|
+
if (!Array.isArray(messages) || messages.length === 0)
|
|
202
|
+
return params;
|
|
203
|
+
const searchUseIds = new Set();
|
|
204
|
+
for (const message of messages) {
|
|
205
|
+
if (message.role !== "assistant" || !Array.isArray(message.content))
|
|
206
|
+
continue;
|
|
207
|
+
for (const block of message.content) {
|
|
208
|
+
if (isRecord(block) && block.type === "tool_search_tool_result" && typeof block.tool_use_id === "string") {
|
|
209
|
+
searchUseIds.add(block.tool_use_id);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
let changed = false;
|
|
214
|
+
const demotedNames = new Set();
|
|
215
|
+
const rewrittenMessages = [];
|
|
216
|
+
for (const message of messages) {
|
|
217
|
+
if (!Array.isArray(message.content)) {
|
|
218
|
+
rewrittenMessages.push(message);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
let messageChanged = false;
|
|
222
|
+
const content = [];
|
|
223
|
+
for (const block of message.content) {
|
|
224
|
+
if (message.role === "assistant" && isRecord(block)) {
|
|
225
|
+
if (block.type === "server_tool_use" && typeof block.id === "string" && searchUseIds.has(block.id)) {
|
|
226
|
+
messageChanged = true;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (block.type === "tool_search_tool_result") {
|
|
230
|
+
messageChanged = true;
|
|
231
|
+
const names = isNativeToolSearchResultBlock(block)
|
|
232
|
+
? toolReferenceNames(block.content.tool_references)
|
|
233
|
+
: [];
|
|
234
|
+
for (const name of names)
|
|
235
|
+
demotedNames.add(name);
|
|
236
|
+
content.push({
|
|
237
|
+
type: "text",
|
|
238
|
+
text: names.length > 0 ? `Tool search found: ${names.join(", ")}` : "Tool search found no tools.",
|
|
239
|
+
});
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (isRecord(block) && block.type === "tool_result" && Array.isArray(block.content)) {
|
|
244
|
+
const names = toolReferenceNames(block.content);
|
|
245
|
+
if (names.length > 0) {
|
|
246
|
+
messageChanged = true;
|
|
247
|
+
for (const name of names)
|
|
248
|
+
demotedNames.add(name);
|
|
249
|
+
const kept = block.content.filter((item) => !(isRecord(item) && item.type === "tool_reference"));
|
|
250
|
+
content.push({
|
|
251
|
+
...block,
|
|
252
|
+
content: [...kept, { type: "text", text: `Tools loaded: ${names.join(", ")}` }],
|
|
253
|
+
});
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
content.push(block);
|
|
258
|
+
}
|
|
259
|
+
if (!messageChanged) {
|
|
260
|
+
rewrittenMessages.push(message);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
changed = true;
|
|
264
|
+
if (content.length > 0)
|
|
265
|
+
rewrittenMessages.push({ ...message, content });
|
|
266
|
+
}
|
|
267
|
+
if (!changed)
|
|
268
|
+
return params;
|
|
269
|
+
return { ...params, messages: rewrittenMessages, ...residentTools(params.tools, demotedNames) };
|
|
270
|
+
}
|
|
271
|
+
/** Drops `defer_loading` from the tools a demoted reference named; the rest keep their deferral. */
|
|
272
|
+
function residentTools(tools, names) {
|
|
273
|
+
if (!Array.isArray(tools) || names.size === 0)
|
|
274
|
+
return {};
|
|
275
|
+
let changed = false;
|
|
276
|
+
const rewritten = tools.map((tool) => {
|
|
277
|
+
if (!isRecord(tool) || tool.defer_loading !== true || typeof tool.name !== "string" || !names.has(tool.name))
|
|
278
|
+
return tool;
|
|
279
|
+
changed = true;
|
|
280
|
+
const { defer_loading: _deferLoading, ...resident } = tool;
|
|
281
|
+
return resident;
|
|
282
|
+
});
|
|
283
|
+
return changed ? { tools: rewritten } : {};
|
|
284
|
+
}
|
|
285
|
+
function toolReferenceNames(items) {
|
|
286
|
+
const names = new Set();
|
|
287
|
+
for (const item of items) {
|
|
288
|
+
if (isRecord(item) && item.type === "tool_reference" && typeof item.tool_name === "string")
|
|
289
|
+
names.add(item.tool_name);
|
|
290
|
+
}
|
|
291
|
+
return [...names];
|
|
292
|
+
}
|
|
186
293
|
/**
|
|
187
294
|
* Folds every `tool_reference` item in `items` onto the request's own tool
|
|
188
295
|
* name and drops the ones that still do not resolve. Returns undefined when
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Tool } from "../types.ts";
|
|
2
|
+
/** Returns true when a provider's strict mode rejects this schema keyword with this value. */
|
|
3
|
+
export type UnsupportedStrictSchemaKeywordCheck = (key: string, value: unknown) => boolean;
|
|
2
4
|
/** Convert a tool schema to the strict subset expected by provider constrained sampling. */
|
|
3
|
-
export declare function makeStrictJsonSchema(schema: Tool["parameters"]): Record<string, unknown>;
|
|
5
|
+
export declare function makeStrictJsonSchema(schema: Tool["parameters"], isUnsupportedKeyword?: UnsupportedStrictSchemaKeywordCheck): Record<string, unknown>;
|
|
4
6
|
export declare function getJsonSchemaToolParameters(tool: Tool, strict: boolean | undefined): Tool["parameters"];
|
|
5
7
|
export interface GrammarConstrainedSampling {
|
|
6
8
|
format: "lark" | "regex";
|
|
@@ -14,7 +16,11 @@ export interface GrammarToolInputJsonBuffer {
|
|
|
14
16
|
}
|
|
15
17
|
export declare function getGrammarToolInput(toolName: string, arguments_: Record<string, unknown>, inputProperty: string): string;
|
|
16
18
|
export declare function appendGrammarToolInputJsonDelta(buffer: GrammarToolInputJsonBuffer, inputProperty: string, nextInput: string, close: boolean): string | undefined;
|
|
17
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Decide whether a JSON-schema tool is sent in strict mode. `isUnsupportedKeyword` lets a provider
|
|
21
|
+
* reject extra keywords its strict mode does not accept, so "prefer" tools fall back to non-strict.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveJsonSchemaStrictSampling(tool: Tool, supportsStrictMode: boolean, isUnsupportedKeyword?: UnsupportedStrictSchemaKeywordCheck): boolean | undefined;
|
|
18
24
|
export declare function resolveGrammarConstrainedSampling(tool: Tool, supportsOpenAIGrammarTools: boolean): GrammarConstrainedSampling | undefined;
|
|
19
25
|
export declare function createGrammarToolInputProperties(tools: Tool[] | undefined, supportsOpenAIGrammarTools: boolean): ReadonlyMap<string, string>;
|
|
20
26
|
//# sourceMappingURL=constrained-sampling.d.ts.map
|
|
@@ -39,7 +39,7 @@ function schemaAllowsNull(schema) {
|
|
|
39
39
|
return true;
|
|
40
40
|
return Array.isArray(schema.anyOf) && schema.anyOf.some((variant) => schemaAllowsNull(variant));
|
|
41
41
|
}
|
|
42
|
-
function makeJsonSchemaNodeStrict(schema) {
|
|
42
|
+
function makeJsonSchemaNodeStrict(schema, isUnsupportedKeyword) {
|
|
43
43
|
if (!isJsonSchemaObject(schema)) {
|
|
44
44
|
throw new UnsupportedStrictJsonSchemaError("boolean schemas are unsupported");
|
|
45
45
|
}
|
|
@@ -48,6 +48,13 @@ function makeJsonSchemaNodeStrict(schema) {
|
|
|
48
48
|
throw new UnsupportedStrictJsonSchemaError(`${key} schemas are unsupported`);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
if (isUnsupportedKeyword) {
|
|
52
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
53
|
+
if (isUnsupportedKeyword(key, value)) {
|
|
54
|
+
throw new UnsupportedStrictJsonSchemaError(`${key}: ${JSON.stringify(value)} is unsupported`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
51
58
|
if (schema.anyOf !== undefined) {
|
|
52
59
|
if (!Array.isArray(schema.anyOf) || schema.anyOf.length === 0) {
|
|
53
60
|
throw new UnsupportedStrictJsonSchemaError("anyOf must contain at least one schema");
|
|
@@ -56,14 +63,14 @@ function makeJsonSchemaNodeStrict(schema) {
|
|
|
56
63
|
if (isStructuredSchema(variant)) {
|
|
57
64
|
throw new UnsupportedStrictJsonSchemaError("object and array unions are unsupported");
|
|
58
65
|
}
|
|
59
|
-
makeJsonSchemaNodeStrict(variant);
|
|
66
|
+
makeJsonSchemaNodeStrict(variant, isUnsupportedKeyword);
|
|
60
67
|
}
|
|
61
68
|
}
|
|
62
69
|
if (schema.items !== undefined) {
|
|
63
70
|
if (Array.isArray(schema.items)) {
|
|
64
71
|
throw new UnsupportedStrictJsonSchemaError("tuple schemas are unsupported");
|
|
65
72
|
}
|
|
66
|
-
makeJsonSchemaNodeStrict(schema.items);
|
|
73
|
+
makeJsonSchemaNodeStrict(schema.items, isUnsupportedKeyword);
|
|
67
74
|
}
|
|
68
75
|
const isObjectSchema = schema.type === "object";
|
|
69
76
|
if (schema.properties !== undefined && !isObjectSchema) {
|
|
@@ -88,7 +95,7 @@ function makeJsonSchemaNodeStrict(schema) {
|
|
|
88
95
|
throw new UnsupportedStrictJsonSchemaError("required contains an unknown property");
|
|
89
96
|
}
|
|
90
97
|
for (const [key, property] of Object.entries(properties)) {
|
|
91
|
-
makeJsonSchemaNodeStrict(property);
|
|
98
|
+
makeJsonSchemaNodeStrict(property, isUnsupportedKeyword);
|
|
92
99
|
if (!required.has(key) && !schemaAllowsNull(property)) {
|
|
93
100
|
properties[key] = { anyOf: [property, { type: "null" }] };
|
|
94
101
|
}
|
|
@@ -97,12 +104,12 @@ function makeJsonSchemaNodeStrict(schema) {
|
|
|
97
104
|
schema.additionalProperties = false;
|
|
98
105
|
}
|
|
99
106
|
/** Convert a tool schema to the strict subset expected by provider constrained sampling. */
|
|
100
|
-
export function makeStrictJsonSchema(schema) {
|
|
107
|
+
export function makeStrictJsonSchema(schema, isUnsupportedKeyword) {
|
|
101
108
|
const cloned = structuredClone(schema);
|
|
102
109
|
if (!isJsonSchemaObject(cloned)) {
|
|
103
110
|
throw new UnsupportedStrictJsonSchemaError("root schema must have type object");
|
|
104
111
|
}
|
|
105
|
-
makeJsonSchemaNodeStrict(cloned);
|
|
112
|
+
makeJsonSchemaNodeStrict(cloned, isUnsupportedKeyword);
|
|
106
113
|
if (cloned.type !== "object") {
|
|
107
114
|
throw new UnsupportedStrictJsonSchemaError("root schema must have type object");
|
|
108
115
|
}
|
|
@@ -160,13 +167,17 @@ function inferGrammarInputProperty(tool) {
|
|
|
160
167
|
}
|
|
161
168
|
return inputProperty;
|
|
162
169
|
}
|
|
163
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Decide whether a JSON-schema tool is sent in strict mode. `isUnsupportedKeyword` lets a provider
|
|
172
|
+
* reject extra keywords its strict mode does not accept, so "prefer" tools fall back to non-strict.
|
|
173
|
+
*/
|
|
174
|
+
export function resolveJsonSchemaStrictSampling(tool, supportsStrictMode, isUnsupportedKeyword) {
|
|
164
175
|
const config = tool.constrainedSampling;
|
|
165
176
|
if (config === false || config?.type !== "json_schema")
|
|
166
177
|
return undefined;
|
|
167
178
|
if (supportsStrictMode) {
|
|
168
179
|
try {
|
|
169
|
-
makeStrictJsonSchema(tool.parameters);
|
|
180
|
+
makeStrictJsonSchema(tool.parameters, isUnsupportedKeyword);
|
|
170
181
|
return true;
|
|
171
182
|
}
|
|
172
183
|
catch (error) {
|
|
@@ -337,12 +337,14 @@ export function convertResponsesMessages(model, context, allowedToolCallProvider
|
|
|
337
337
|
// An active grammar declaration wins over sentinel recovery below: its
|
|
338
338
|
// named input property is richer than the persisted freeform fallback.
|
|
339
339
|
// For different-model messages, set id to undefined to avoid pairing validation.
|
|
340
|
-
// OpenAI tracks which
|
|
340
|
+
// OpenAI tracks which item IDs were paired with rs_xxx reasoning items.
|
|
341
341
|
// By omitting the id, we avoid triggering that validation (like cross-provider does).
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
|
|
342
|
+
// Also drop ids that do not match the replayed item type: function_call ids must be fc_*
|
|
343
|
+
// and custom_tool_call ids must be ctc_*. Foreign tool call ids are normalized to fc_*, and
|
|
344
|
+
// a call can switch between the two types when grammar tool support differs. Freeform
|
|
345
|
+
// calls replay without an item id and without the local <call_id>|custom sentinel.
|
|
346
|
+
const itemIdPrefix = customInputProperty === undefined ? "fc_" : "ctc_";
|
|
347
|
+
if (isDifferentModel || !itemId?.startsWith(itemIdPrefix)) {
|
|
346
348
|
itemId = undefined;
|
|
347
349
|
}
|
|
348
350
|
const canReplayNamespace = isSameModel || toolPlacement.deferred.has(toolCall.name);
|
package/dist/api-registry.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface ApiProviderInternal {
|
|
|
14
14
|
export type RegisteredApiProvider = {
|
|
15
15
|
provider: ApiProviderInternal;
|
|
16
16
|
sourceId?: string;
|
|
17
|
+
/** Kept by `clearApiProviders()`; removed only through `unregisterApiProviders(sourceId)`. */
|
|
18
|
+
survivesClear?: boolean;
|
|
17
19
|
};
|
|
18
20
|
/** Browser-neutral shape installed by the node-only provider-scope subpath. */
|
|
19
21
|
export interface ProviderScopeAccess {
|
|
@@ -23,7 +25,9 @@ export interface ProviderScopeAccess {
|
|
|
23
25
|
/** Installs the optional node-only scope lookup without importing node APIs here. */
|
|
24
26
|
export declare function installProviderScopeAccessor(accessor: () => ProviderScopeAccess | undefined): void;
|
|
25
27
|
export declare function setProviderScopeStrictMode(enabled: boolean): void;
|
|
26
|
-
export declare function registerApiProvider<TApi extends Api, TOptions extends StreamOptions>(provider: ApiProvider<TApi, TOptions>, sourceId?: string
|
|
28
|
+
export declare function registerApiProvider<TApi extends Api, TOptions extends StreamOptions>(provider: ApiProvider<TApi, TOptions>, sourceId?: string, options?: {
|
|
29
|
+
survivesClear?: boolean;
|
|
30
|
+
}): void;
|
|
27
31
|
/** Registers a builtin once, retaining its immutable identity for active scopes. */
|
|
28
32
|
export declare function registerBuiltinApiProvider<TApi extends Api, TOptions extends StreamOptions>(provider: ApiProvider<TApi, TOptions>): void;
|
|
29
33
|
export declare function getBuiltinApiProvider(api: Api): ApiProviderInternal | undefined;
|