@gajae-code/ai 0.4.5 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/dist/types/index.d.ts +1 -0
  3. package/dist/types/providers/amazon-bedrock.d.ts +29 -5
  4. package/dist/types/providers/composer-discipline.d.ts +27 -0
  5. package/dist/types/providers/cursor.d.ts +1 -1
  6. package/dist/types/providers/google-gemini-cli.d.ts +1 -1
  7. package/dist/types/providers/google-shared.d.ts +11 -1
  8. package/dist/types/providers/ollama.d.ts +36 -1
  9. package/dist/types/providers/openai-completions-compat.d.ts +3 -1
  10. package/dist/types/providers/register-builtins.d.ts +3 -3
  11. package/dist/types/types.d.ts +25 -3
  12. package/dist/types/utils/event-stream.d.ts +6 -1
  13. package/dist/types/utils/tool-choice-capability.d.ts +41 -0
  14. package/package.json +2 -2
  15. package/src/index.ts +1 -0
  16. package/src/model-thinking.ts +9 -0
  17. package/src/models.json +92 -0
  18. package/src/models.ts +33 -7
  19. package/src/provider-models/openai-compat.ts +9 -1
  20. package/src/providers/amazon-bedrock.ts +145 -60
  21. package/src/providers/anthropic.ts +85 -32
  22. package/src/providers/azure-openai-responses.ts +44 -3
  23. package/src/providers/composer-discipline.ts +38 -0
  24. package/src/providers/cursor.ts +10 -3
  25. package/src/providers/google-gemini-cli.ts +69 -10
  26. package/src/providers/google-shared.ts +61 -12
  27. package/src/providers/ollama.ts +60 -4
  28. package/src/providers/openai-codex-responses.ts +151 -2
  29. package/src/providers/openai-completions-compat.ts +9 -1
  30. package/src/providers/openai-completions.ts +46 -6
  31. package/src/providers/openai-request-transform.ts +1 -0
  32. package/src/providers/openai-responses.ts +54 -5
  33. package/src/providers/register-builtins.ts +5 -6
  34. package/src/types.ts +37 -3
  35. package/src/utils/event-stream.ts +35 -5
  36. package/src/utils/tool-choice-capability.ts +220 -0
@@ -39,6 +39,7 @@ const OPENAI_PROXY_STRIP_HEADERS = [
39
39
  "x-stainless-helper-method",
40
40
  "openai-organization",
41
41
  "openai-project",
42
+ "openai-beta",
42
43
  ] as const;
43
44
 
44
45
  function resolveRequestTransform(
@@ -1,4 +1,11 @@
1
- import { $env, $inheritedEnv, extractHttpStatusFromError, structuredCloneJSON } from "@gajae-code/utils";
1
+ import {
2
+ $credentialEnv,
3
+ $env,
4
+ $inheritedEnv,
5
+ extractHttpStatusFromError,
6
+ logger,
7
+ structuredCloneJSON,
8
+ } from "@gajae-code/utils";
2
9
  import OpenAI from "openai";
3
10
  import type {
4
11
  Tool as OpenAITool,
@@ -46,6 +53,11 @@ import { resolveRetryBudget } from "../utils/retry-budget";
46
53
  import { adaptSchemaForStrict, NO_STRICT, sanitizeSchemaForOpenAIResponses, toolWireSchema } from "../utils/schema";
47
54
  import { wrapFetchForSseDebug } from "../utils/sse-debug";
48
55
  import { mapToOpenAIResponsesToolChoice, type OpenAIResponsesToolChoice } from "../utils/tool-choice";
56
+ import {
57
+ isForcedToolChoiceUnsupportedError,
58
+ markToolChoiceIncapability,
59
+ resolveToolChoice,
60
+ } from "../utils/tool-choice-capability";
49
61
  import {
50
62
  buildCopilotDynamicHeaders,
51
63
  hasCopilotVisionInput,
@@ -278,7 +290,31 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
278
290
  return data;
279
291
  },
280
292
  { provider: model.provider, signal: requestSignal },
281
- );
293
+ ).catch(async error => {
294
+ if (!isForcedToolChoiceUnsupportedError(error, isForcedOpenAIResponsesToolChoice(params.tool_choice))) {
295
+ throw error;
296
+ }
297
+ const reason = await finalizeErrorMessage(error, rawRequestDump);
298
+ markToolChoiceIncapability(model, "auto", reason);
299
+ const resolvedToolChoice = resolveToolChoice(model, options?.toolChoice);
300
+ stream.push({
301
+ type: "toolChoiceIncapability",
302
+ api: model.api,
303
+ provider: model.provider,
304
+ model: model.id,
305
+ requestedLevel: resolvedToolChoice.requestedLevel,
306
+ resolvedLevel: "auto",
307
+ reason,
308
+ registryKey: resolvedToolChoice.registryKey,
309
+ });
310
+ delete params.tool_choice;
311
+ if (rawRequestDump) rawRequestDump.body = params;
312
+ const { data, response, request_id } = await client.responses
313
+ .create(params, { signal: requestSignal })
314
+ .withResponse();
315
+ await notifyProviderResponse(options, response, model, request_id);
316
+ return data;
317
+ });
282
318
  const firstEventWatchdog = createWatchdog(
283
319
  options?.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs(idleTimeoutMs),
284
320
  () => abortTracker.abortLocally(firstEventTimeoutAbortError),
@@ -363,12 +399,12 @@ function createClient(
363
399
  baseUrl: string | undefined;
364
400
  } {
365
401
  if (!apiKey) {
366
- if (!$env.OPENAI_API_KEY) {
402
+ apiKey = $credentialEnv("OPENAI_API_KEY");
403
+ if (!apiKey) {
367
404
  throw new Error(
368
405
  "OpenAI API key is required. Set OPENAI_API_KEY environment variable or pass it as an argument.",
369
406
  );
370
407
  }
371
- apiKey = $env.OPENAI_API_KEY;
372
408
  }
373
409
  const rawApiKey = apiKey;
374
410
 
@@ -490,7 +526,16 @@ function buildParams(
490
526
  if (context.tools) {
491
527
  params.tools = convertTools(context.tools, supportsStrictMode(model), model);
492
528
  if (options?.toolChoice) {
493
- params.tool_choice = mapOpenAIResponsesToolChoiceForTools(options.toolChoice, context.tools, model);
529
+ const toolChoice = resolveToolChoice(model, options.toolChoice);
530
+ if (toolChoice.degraded && toolChoice.supportSource === "runtime") {
531
+ logger.debug("openai-responses: degraded tool_choice after runtime capability discovery", {
532
+ model: model.id,
533
+ requestedLevel: toolChoice.requestedLevel,
534
+ resolvedLevel: toolChoice.resolvedLevel,
535
+ reason: toolChoice.reason,
536
+ });
537
+ }
538
+ params.tool_choice = mapOpenAIResponsesToolChoiceForTools(toolChoice.resolvedChoice, context.tools, model);
494
539
  }
495
540
  // The apply_patch spec §1 marks only `apply_patch` itself as
496
541
  // `supports_parallel_tool_calls = false`. OpenAI's Responses API
@@ -651,6 +696,10 @@ export function mapOpenAIResponsesToolChoiceForTools(
651
696
  return customTool ? { type: "custom", name: customTool.customWireName ?? customTool.name } : mapped;
652
697
  }
653
698
 
699
+ function isForcedOpenAIResponsesToolChoice(choice: unknown): boolean {
700
+ return !!choice && choice !== "none" && choice !== "auto";
701
+ }
702
+
654
703
  /** @internal Exported for tests. */
655
704
  export function convertTools(tools: Tool[], strictMode: boolean, model: Model<"openai-responses">): OpenAITool[] {
656
705
  const allowFreeform = supportsFreeformApplyPatch(model);
@@ -6,9 +6,9 @@
6
6
  * openai) at startup. The loaded module promise is cached so subsequent calls
7
7
  * reuse the same import.
8
8
  *
9
- * NOTE: stream.ts currently imports providers directly, so this file is not yet
10
- * wired into the main streaming path. It provides the infrastructure for lazy
11
- * loading that can be integrated when stream.ts is refactored.
9
+ * stream.ts imports its provider stream functions from this module (see the
10
+ * lazy wrappers below), so this file IS the main streaming path's provider
11
+ * loader: heavy SDKs stay out of the CLI startup parse graph.
12
12
  */
13
13
  import type {
14
14
  Api,
@@ -390,9 +390,8 @@ function loadBedrockProviderModule(): Promise<LazyProviderModule<"bedrock-conver
390
390
  // ---------------------------------------------------------------------------
391
391
  // Lazy stream function exports
392
392
  //
393
- // These use the same names as the direct provider stream functions. When
394
- // stream.ts is updated to import from this module instead of individual
395
- // providers, the lazy loading will take effect on the main code path.
393
+ // Provider registry code imports these wrappers so the concrete provider modules
394
+ // are loaded on first use instead of during package initialization.
396
395
  // ---------------------------------------------------------------------------
397
396
 
398
397
  export const streamAnthropic = createLazyStream(loadAnthropicProviderModule);
package/src/types.ts CHANGED
@@ -161,6 +161,18 @@ export type ToolChoice =
161
161
  | { type: "function"; function: { name: string } }
162
162
  | { type: "tool"; name: string };
163
163
 
164
+ export type ToolChoiceSupport = "none" | "auto" | "required" | "named";
165
+ export type ToolChoiceSupportSource = "static" | "derived" | "runtime";
166
+
167
+ export interface ToolChoiceCompat {
168
+ /** Maximum supported tool_choice level. */
169
+ toolChoiceSupport?: ToolChoiceSupport;
170
+ /** Legacy flag for accepting the tool_choice parameter. */
171
+ supportsToolChoice?: boolean;
172
+ /** Legacy flag for forced tool_choice support. */
173
+ supportsForcedToolChoice?: boolean;
174
+ }
175
+
164
176
  // Base options all providers share
165
177
  export type CacheRetention = "none" | "short" | "long";
166
178
 
@@ -705,13 +717,24 @@ export type AssistantMessageEvent =
705
717
  contentIndex?: undefined;
706
718
  reason: Extract<StopReason, "aborted" | "error">;
707
719
  error: AssistantMessage;
720
+ }
721
+ | {
722
+ type: "toolChoiceIncapability";
723
+ contentIndex?: undefined;
724
+ api: string;
725
+ provider: string;
726
+ model: string;
727
+ requestedLevel: ToolChoiceSupport;
728
+ resolvedLevel: ToolChoiceSupport;
729
+ reason: string;
730
+ registryKey: string;
708
731
  };
709
732
 
710
733
  /**
711
734
  * Compatibility settings for openai-completions API.
712
735
  * Use this to override URL-based auto-detection for custom providers.
713
736
  */
714
- export interface OpenAICompat {
737
+ export interface OpenAICompat extends ToolChoiceCompat {
715
738
  /** Whether the provider supports the `store` field. Default: auto-detected from URL. */
716
739
  supportsStore?: boolean;
717
740
  /** Whether the provider supports the `developer` role (vs `system`). Default: auto-detected from URL. */
@@ -757,6 +780,8 @@ export interface OpenAICompat {
757
780
  requiresAssistantContentForToolCalls?: boolean;
758
781
  /** Whether the provider supports the `tool_choice` parameter. Default: true. */
759
782
  supportsToolChoice?: boolean;
783
+ /** Whether `tool_choice` may force a tool (`required` / named tool). Default: true. */
784
+ supportsForcedToolChoice?: boolean;
760
785
  /**
761
786
  * Drop reasoning fields (`reasoning_effort`, OpenRouter `reasoning`) for
762
787
  * the request when `tool_choice` forces a tool call. Mirrors the Anthropic
@@ -789,7 +814,7 @@ export interface OpenAICompat {
789
814
  * Use this to disable features that strict-by-default Anthropic accepts but
790
815
  * that proxy gateways (Vertex AI, AWS Bedrock-style fronts, etc.) reject.
791
816
  */
792
- export interface AnthropicCompat {
817
+ export interface AnthropicCompat extends ToolChoiceCompat {
793
818
  /**
794
819
  * Drop the top-level `strict: true` field on tool definitions. Vertex AI's
795
820
  * Anthropic-compatible endpoint rejects unknown tool fields with
@@ -911,7 +936,16 @@ export interface Model<TApi extends Api = any> {
911
936
  ? OpenAICompat
912
937
  : TApi extends "anthropic-messages"
913
938
  ? AnthropicCompat
914
- : never;
939
+ : TApi extends
940
+ | "bedrock-converse-stream"
941
+ | "google-generative-ai"
942
+ | "google-gemini-cli"
943
+ | "google-vertex"
944
+ | "ollama-chat"
945
+ | "azure-openai-responses"
946
+ | "openai-codex-responses"
947
+ ? ToolChoiceCompat
948
+ : never;
915
949
  /**
916
950
  * Which shape to use when exposing the OpenAI code backend `apply_patch` tool to this model.
917
951
  * Generated catalog policy sets `"freeform"` for first-party GPT-5 Responses
@@ -2,7 +2,8 @@ import type { AssistantMessage, AssistantMessageEvent } from "../types";
2
2
 
3
3
  // Generic event stream class for async iteration
4
4
  export class EventStream<T, R = T> implements AsyncIterable<T> {
5
- queue: T[] = [];
5
+ #queue: T[] = [];
6
+ #queueHead = 0;
6
7
  waiting: Array<{ resolve: (value: IteratorResult<T>) => void; reject: (err: unknown) => void }> = [];
7
8
  done = false;
8
9
  #failed = false;
@@ -25,6 +26,35 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
25
26
  this.extractResult = extractResult;
26
27
  }
27
28
 
29
+ #enqueue(event: T): void {
30
+ this.#queue.push(event);
31
+ }
32
+
33
+ #dequeue(): T | undefined {
34
+ if (this.#queueHead >= this.#queue.length) return undefined;
35
+ const event = this.#queue[this.#queueHead]!;
36
+ this.#queue[this.#queueHead] = undefined as T;
37
+ this.#queueHead++;
38
+ if (this.#queueHead > 1024 && this.#queueHead * 2 >= this.#queue.length) {
39
+ this.#queue = this.#queue.slice(this.#queueHead);
40
+ this.#queueHead = 0;
41
+ }
42
+ return event;
43
+ }
44
+
45
+ get #queueLength(): number {
46
+ return this.#queue.length - this.#queueHead;
47
+ }
48
+
49
+ /**
50
+ * Read-only snapshot of the not-yet-consumed events. Always a fresh copy:
51
+ * external code can never mutate internal queue state or observe head-index
52
+ * tombstones, so the deque cannot desynchronize.
53
+ */
54
+ get queue(): T[] {
55
+ return this.#queue.slice(this.#queueHead);
56
+ }
57
+
28
58
  push(event: T): void {
29
59
  if (this.done) return;
30
60
 
@@ -38,7 +68,7 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
38
68
  if (waiter) {
39
69
  waiter.resolve({ value: event, done: false });
40
70
  } else {
41
- this.queue.push(event);
71
+ this.#enqueue(event);
42
72
  }
43
73
  }
44
74
 
@@ -47,7 +77,7 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
47
77
  if (waiter) {
48
78
  waiter.resolve({ value: event, done: false });
49
79
  } else {
50
- this.queue.push(event);
80
+ this.#enqueue(event);
51
81
  }
52
82
  }
53
83
 
@@ -84,8 +114,8 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
84
114
 
85
115
  async *[Symbol.asyncIterator](): AsyncIterator<T> {
86
116
  while (true) {
87
- if (this.queue.length > 0) {
88
- yield this.queue.shift()!;
117
+ if (this.#queueLength > 0) {
118
+ yield this.#dequeue()!;
89
119
  } else if (this.#failed) {
90
120
  throw this.#error;
91
121
  } else if (this.done) {
@@ -0,0 +1,220 @@
1
+ import { extractHttpStatusFromError } from "@gajae-code/utils/fetch-retry";
2
+ import * as logger from "@gajae-code/utils/logger";
3
+ import type { Api, Model, ToolChoice, ToolChoiceCompat, ToolChoiceSupport, ToolChoiceSupportSource } from "../types";
4
+
5
+ const supportRank: Record<ToolChoiceSupport, number> = {
6
+ none: 0,
7
+ auto: 1,
8
+ required: 2,
9
+ named: 3,
10
+ };
11
+
12
+ const registry = new Map<string, ToolChoiceSupport>();
13
+ const loggedRegistryKeys = new Set<string>();
14
+
15
+ /**
16
+ * Claude Mythos accepts tools but rejects forced tool use (Anthropic 400:
17
+ * "tool_choice forces tool use is not compatible with this model"). Catalog
18
+ * generation and dynamic discovery use this to default `toolChoiceSupport`.
19
+ */
20
+ export function isClaudeForcedToolChoiceIncapableModelId(modelId: string): boolean {
21
+ return /(?:^|[/.])claude-mythos(?:-|$)/i.test(modelId);
22
+ }
23
+
24
+ /** Derives the effective static tool-choice support from compatibility flags. */
25
+ export function deriveToolChoiceSupport(compat: ToolChoiceCompat | undefined): {
26
+ support: ToolChoiceSupport;
27
+ source: "static" | "derived";
28
+ } {
29
+ if (compat?.toolChoiceSupport) {
30
+ return { support: compat.toolChoiceSupport, source: "static" };
31
+ }
32
+ if (compat?.supportsToolChoice === false) {
33
+ return { support: "none", source: "derived" };
34
+ }
35
+ if (compat?.supportsForcedToolChoice === false) {
36
+ return { support: "auto", source: "derived" };
37
+ }
38
+ return { support: "named", source: "derived" };
39
+ }
40
+
41
+ /** Returns the registry key used for runtime tool-choice capability overrides. */
42
+ export function toolChoiceRegistryKey(model: Model<Api>): string {
43
+ return [model.api, model.provider, model.baseUrl, model.wireModelId ?? model.id].join("|");
44
+ }
45
+
46
+ /** Returns the current runtime tool-choice capability override for a model. */
47
+ export function getToolChoiceCapabilityOverride(model: Model<Api>): ToolChoiceSupport | undefined {
48
+ return registry.get(toolChoiceRegistryKey(model));
49
+ }
50
+
51
+ /** Clears runtime tool-choice capability overrides for tests. */
52
+ export function clearToolChoiceIncapabilityRegistryForTests(): void {
53
+ registry.clear();
54
+ loggedRegistryKeys.clear();
55
+ }
56
+
57
+ /** Records a discovered maximum supported tool-choice level for a model. */
58
+ export function markToolChoiceIncapability(model: Model<Api>, maxSupport: ToolChoiceSupport, reason?: string): void {
59
+ const key = toolChoiceRegistryKey(model);
60
+ const existing = registry.get(key);
61
+ const next = existing && supportRank[existing] < supportRank[maxSupport] ? existing : maxSupport;
62
+ registry.set(key, next);
63
+
64
+ if (!loggedRegistryKeys.has(key)) {
65
+ loggedRegistryKeys.add(key);
66
+ logger.debug("Discovered tool_choice incapability", {
67
+ api: model.api,
68
+ provider: model.provider,
69
+ baseUrlHost: safeHostname(model.baseUrl),
70
+ model: model.wireModelId ?? model.id,
71
+ maxSupport,
72
+ reason,
73
+ });
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Resolves a requested tool_choice against static and runtime capability limits.
79
+ * `compat` overrides `model.compat` for transports that layer URL/provider
80
+ * detection on top of explicit model overrides (e.g. resolveOpenAICompat).
81
+ */
82
+ export function resolveToolChoice(
83
+ model: Model<Api>,
84
+ requested: ToolChoice | undefined,
85
+ compat?: ToolChoiceCompat,
86
+ ): ResolveToolChoiceResult {
87
+ const derived = deriveToolChoiceSupport(compat ?? model.compat);
88
+ const runtime = registry.get(toolChoiceRegistryKey(model));
89
+ const support = runtime && supportRank[runtime] < supportRank[derived.support] ? runtime : derived.support;
90
+ const supportSource: ToolChoiceSupportSource = support === derived.support ? derived.source : "runtime";
91
+ const requestedInfo = requestedToolChoiceLevel(requested);
92
+ const clampLevel = requestedInfo.requestedLevel === "none" ? "auto" : requestedInfo.requestedLevel;
93
+ const registryKey = toolChoiceRegistryKey(model);
94
+
95
+ if (requested === undefined) {
96
+ return {
97
+ requestedChoice: requested,
98
+ requestedLevel: requestedInfo.requestedLevel,
99
+ resolvedChoice: undefined,
100
+ resolvedLevel: "auto",
101
+ support,
102
+ supportSource,
103
+ degraded: false,
104
+ registryKey,
105
+ };
106
+ }
107
+
108
+ if (support === "none") {
109
+ return {
110
+ requestedChoice: requested,
111
+ requestedLevel: requestedInfo.requestedLevel,
112
+ resolvedChoice: undefined,
113
+ resolvedLevel: "none",
114
+ support,
115
+ supportSource,
116
+ degraded: requestedInfo.requestedLevel !== "none",
117
+ reason: "tool_choice is not supported by this model",
118
+ registryKey,
119
+ targetToolName: requestedInfo.targetToolName,
120
+ };
121
+ }
122
+
123
+ if (supportRank[support] >= supportRank[clampLevel]) {
124
+ return {
125
+ requestedChoice: requested,
126
+ requestedLevel: requestedInfo.requestedLevel,
127
+ resolvedChoice: requested,
128
+ resolvedLevel: requestedInfo.requestedLevel,
129
+ support,
130
+ supportSource,
131
+ degraded: false,
132
+ registryKey,
133
+ targetToolName: requestedInfo.targetToolName,
134
+ };
135
+ }
136
+
137
+ if (requestedInfo.requestedLevel === "named" && support === "required") {
138
+ return {
139
+ requestedChoice: requested,
140
+ requestedLevel: "named",
141
+ resolvedChoice: "required",
142
+ resolvedLevel: "required",
143
+ support,
144
+ supportSource,
145
+ degraded: true,
146
+ reason: "named tool_choice degraded to required",
147
+ registryKey,
148
+ targetToolName: requestedInfo.targetToolName,
149
+ };
150
+ }
151
+
152
+ return {
153
+ requestedChoice: requested,
154
+ requestedLevel: requestedInfo.requestedLevel,
155
+ resolvedChoice: undefined,
156
+ resolvedLevel: support === "auto" ? "auto" : "none",
157
+ support,
158
+ supportSource,
159
+ degraded: true,
160
+ reason: "forced tool_choice is not supported by this model",
161
+ registryKey,
162
+ targetToolName: requestedInfo.targetToolName,
163
+ };
164
+ }
165
+
166
+ /** Detects provider errors indicating forced tool_choice is unsupported. */
167
+ export function isForcedToolChoiceUnsupportedError(error: unknown, sentForcedToolChoice: boolean): boolean {
168
+ if (!sentForcedToolChoice || extractHttpStatusFromError(error) !== 400) return false;
169
+ const message = errorMessage(error);
170
+ return (
171
+ // `by <something>` continuations ("not supported by billing") describe a
172
+ // different subject than the model's tool_choice capability — reject them
173
+ // unless the continuation names the model itself.
174
+ /tool[_\s-]?choices?\b.*?(not\s+compatible|incompatible|not\s+supported)(?!\s+by\s+(?!(?:this|the)\s+model\b|model\b))/is.test(
175
+ message,
176
+ ) ||
177
+ /forces?\s+tool\s+use.*?(not\s+compatible|incompatible|not\s+supported)/is.test(message) ||
178
+ /does\s+not\s+support\s+forced\s+tool[_\s-]?choices?/is.test(message)
179
+ );
180
+ }
181
+
182
+ export type { ToolChoiceCompat, ToolChoiceSupport, ToolChoiceSupportSource } from "../types";
183
+
184
+ export interface ResolveToolChoiceResult {
185
+ requestedChoice: ToolChoice | undefined;
186
+ requestedLevel: ToolChoiceSupport;
187
+ resolvedChoice: ToolChoice | undefined;
188
+ resolvedLevel: ToolChoiceSupport;
189
+ support: ToolChoiceSupport;
190
+ supportSource: ToolChoiceSupportSource;
191
+ degraded: boolean;
192
+ reason?: string;
193
+ registryKey: string;
194
+ targetToolName?: string;
195
+ }
196
+
197
+ function requestedToolChoiceLevel(requested: ToolChoice | undefined): {
198
+ requestedLevel: ToolChoiceSupport;
199
+ targetToolName?: string;
200
+ } {
201
+ if (requested === undefined || requested === "auto") return { requestedLevel: "auto" };
202
+ if (requested === "none") return { requestedLevel: "none" };
203
+ if (requested === "any" || requested === "required") return { requestedLevel: "required" };
204
+ if ("name" in requested) return { requestedLevel: "named", targetToolName: requested.name };
205
+ return { requestedLevel: "named", targetToolName: requested.function.name };
206
+ }
207
+
208
+ function errorMessage(error: unknown): string {
209
+ if (error instanceof Error) return error.message;
210
+ if (typeof error === "string") return error;
211
+ return String(error);
212
+ }
213
+
214
+ function safeHostname(baseUrl: string): string | undefined {
215
+ try {
216
+ return new URL(baseUrl).hostname;
217
+ } catch {
218
+ return undefined;
219
+ }
220
+ }