





















1+import { projectRuntimeToolInputSchema } from "./tool-schema-json-projection.js";
2+3+type AnthropicToolDescriptor = {
4+readonly name: string;
5+readonly description: string;
6+readonly parameters: unknown;
7+};
8+9+export type AnthropicProjectedTool = {
10+readonly originalName: string;
11+readonly wireName: string;
12+readonly description?: string;
13+readonly inputSchema: {
14+readonly type: "object";
15+readonly properties: Record<string, unknown>;
16+readonly required: string[];
17+};
18+};
19+20+export type AnthropicToolProjection = {
21+readonly inputToolCount: number;
22+readonly unavailableOriginalNames: ReadonlySet<string>;
23+readonly tools: readonly AnthropicProjectedTool[];
24+};
25+26+type AnthropicParallelToolChoice = {
27+readonly disable_parallel_tool_use?: boolean;
28+};
29+30+export type AnthropicProjectedToolChoice =
31+| ({ readonly type: "auto" } & AnthropicParallelToolChoice)
32+| ({ readonly type: "any" } & AnthropicParallelToolChoice)
33+| { readonly type: "none" }
34+| ({ readonly type: "tool"; readonly name: string } & AnthropicParallelToolChoice);
35+36+function isRecord(value: unknown): value is Record<string, unknown> {
37+return Boolean(value) && typeof value === "object" && !Array.isArray(value);
38+}
39+40+function isProviderSupportedViolation(violation: string): boolean {
41+return violation.endsWith(".$dynamicRef") || violation.endsWith(".$dynamicAnchor");
42+}
43+44+/** Snapshots direct/custom tool descriptors before Anthropic payload construction. */
45+export function projectAnthropicTools(
46+tools: readonly AnthropicToolDescriptor[],
47+toWireName: (name: string) => string,
48+): AnthropicToolProjection {
49+const projectedTools: AnthropicProjectedTool[] = [];
50+const unavailableOriginalNames = new Set<string>();
51+for (const tool of tools) {
52+let projectedTool: AnthropicProjectedTool;
53+let originalName: string | undefined;
54+try {
55+const name = tool.name;
56+originalName = name;
57+if (!name) {
58+continue;
59+}
60+const schemaProjection = projectRuntimeToolInputSchema(tool.parameters, `${name}.parameters`);
61+if (
62+!isRecord(schemaProjection.schema) ||
63+schemaProjection.violations.some((violation) => !isProviderSupportedViolation(violation))
64+) {
65+unavailableOriginalNames.add(name);
66+continue;
67+}
68+const properties = schemaProjection.schema.properties;
69+const required = schemaProjection.schema.required;
70+if (
71+(properties !== undefined && properties !== null && !isRecord(properties)) ||
72+(required !== undefined &&
73+required !== null &&
74+(!Array.isArray(required) || required.some((entry) => typeof entry !== "string")))
75+) {
76+unavailableOriginalNames.add(name);
77+continue;
78+}
79+let description: string | undefined;
80+try {
81+description = typeof tool.description === "string" ? tool.description : undefined;
82+} catch {
83+// Description is optional; keep the usable tool schema.
84+}
85+const wireName = toWireName(name);
86+projectedTool = {
87+originalName: name,
88+ wireName,
89+ ...(description ? { description } : {}),
90+inputSchema: {
91+type: "object",
92+properties: (properties ?? {}) as Record<string, unknown>,
93+required: (required ?? []) as string[],
94+},
95+};
96+} catch {
97+// Direct/custom tool arrays can bypass the runtime quarantine.
98+if (originalName) {
99+unavailableOriginalNames.add(originalName);
100+}
101+continue;
102+}
103+const conflictingTool = projectedTools.find(
104+(entry) => entry.wireName === projectedTool.wireName,
105+);
106+if (conflictingTool && conflictingTool.originalName !== projectedTool.originalName) {
107+throw new Error(
108+`Anthropic tool names "${conflictingTool.originalName}" and "${projectedTool.originalName}" both map to "${projectedTool.wireName}"`,
109+);
110+}
111+projectedTools.push(projectedTool);
112+}
113+return {
114+inputToolCount: tools.length,
115+ unavailableOriginalNames,
116+tools: projectedTools,
117+};
118+}
119+120+/** Keeps forced Anthropic tool choices aligned with the projected wire names. */
121+export function reconcileAnthropicToolChoice(
122+choice: AnthropicProjectedToolChoice,
123+projection: AnthropicToolProjection,
124+): AnthropicProjectedToolChoice | undefined {
125+if (projection.inputToolCount === 0) {
126+return choice;
127+}
128+if (choice.type === "tool") {
129+const requestedName = choice.name;
130+const originalMatch = projection.tools.find((tool) => tool.originalName === requestedName);
131+if (originalMatch) {
132+return { ...choice, name: originalMatch.wireName };
133+}
134+if (projection.unavailableOriginalNames.has(requestedName)) {
135+throw new Error(
136+`Anthropic tool_choice requested unavailable tool "${requestedName}" after schema conversion`,
137+);
138+}
139+const matchedTool = projection.tools.find((tool) => tool.wireName === requestedName);
140+if (!matchedTool) {
141+throw new Error(
142+`Anthropic tool_choice requested unavailable tool "${requestedName}" after schema conversion`,
143+);
144+}
145+return { ...choice, name: matchedTool.wireName };
146+}
147+if (projection.tools.length === 0) {
148+if (choice.type === "auto") {
149+return undefined;
150+}
151+if (choice.type === "any") {
152+throw new Error(
153+"Anthropic tool_choice requires a tool, but no tools survived schema conversion",
154+);
155+}
156+}
157+return choice;
158+}
159+160+/** Maps Claude Code wire names without trusting every direct/custom descriptor. */
161+export function resolveOriginalAnthropicToolName(
162+name: string,
163+projection: AnthropicToolProjection | undefined,
164+): string {
165+return projection?.tools.find((tool) => tool.wireName === name)?.originalName ?? name;
166+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。