@oh-my-pi/pi-ai 14.7.8 → 14.8.1

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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [14.8.0] - 2026-05-09
6
+
7
+ ### Fixed
8
+ - Fixed Gemini 3 Pro thinking metadata so `medium` effort is rejected with the expected error instead of being silently accepted: `ThinkingConfig` now carries an optional explicit `levels` list that survives `expandEffortRange`, letting non-contiguous supported sets (e.g. `[low, high]`) round-trip through enrichment.
9
+ - Fixed Kimi Code OAuth expiry handling to refresh access tokens 5 minutes before server expiry, avoiding daily 401s from using tokens right up to the cutoff.
10
+ - Fixed OpenAI Responses custom tool replay to preserve custom tool call item IDs with the `ctc_` prefix instead of rewriting them as `fc_` function-call IDs ([#977](https://github.com/can1357/oh-my-pi/issues/977)).
11
+
5
12
  ## [14.7.6] - 2026-05-07
6
13
 
7
14
  ### Added
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "14.7.8",
4
+ "version": "14.8.1",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -46,8 +46,8 @@
46
46
  "@aws-sdk/credential-provider-node": "^3.972.39",
47
47
  "@bufbuild/protobuf": "^2.12.0",
48
48
  "@google/genai": "^1.52.0",
49
- "@oh-my-pi/pi-natives": "14.7.8",
50
- "@oh-my-pi/pi-utils": "14.7.8",
49
+ "@oh-my-pi/pi-natives": "14.8.1",
50
+ "@oh-my-pi/pi-utils": "14.8.1",
51
51
  "@sinclair/typebox": "^0.34.49",
52
52
  "@smithy/node-http-handler": "^4.6.1",
53
53
  "ajv": "^8.20.0",
@@ -182,8 +182,11 @@ export function linkOpenAIPromotionTargets(models: ApiModel<Api>[]): void {
182
182
  }
183
183
 
184
184
  /**
185
- * Returns supported thinking efforts from canonical model rules constrained by
186
- * explicit model metadata.
185
+ * Returns the supported thinking efforts declared on the model metadata.
186
+ *
187
+ * Catalog enrichment is responsible for normalizing bundled model metadata up front.
188
+ * Runtime callers must treat explicit `model.thinking` on custom models as authoritative
189
+ * so proxy-specific overrides from `models.yml` survive request construction.
187
190
  *
188
191
  * @throws Error when a reasoning-capable model is missing thinking metadata
189
192
  */
@@ -194,12 +197,7 @@ export function getSupportedEfforts<TApi extends Api>(model: ApiModel<TApi>): re
194
197
  if (!model.thinking) {
195
198
  throw new Error(`Model ${model.provider}/${model.id} is missing thinking metadata`);
196
199
  }
197
- const configuredEfforts = expandEffortRange(model.thinking);
198
- const parsedModel = parseKnownModel(model.id);
199
- if (parsedModel.family === "unknown") {
200
- return configuredEfforts;
201
- }
202
- return intersectEfforts(configuredEfforts, inferSupportedEfforts(parsedModel, model));
200
+ return expandEffortRange(model.thinking);
203
201
  }
204
202
 
205
203
  /**
@@ -316,6 +314,19 @@ function applyGeneratedModelPolicy(model: ApiModel<Api>): void {
316
314
  model.maxTokens = copilotLimits.maxTokens;
317
315
  }
318
316
 
317
+ if (
318
+ model.api === "openai-completions" &&
319
+ (model.provider === "minimax-code" || model.provider === "minimax-code-cn")
320
+ ) {
321
+ model.compat = {
322
+ ...model.compat,
323
+ supportsStore: false,
324
+ supportsDeveloperRole: false,
325
+ supportsReasoningEffort: false,
326
+ reasoningContentField: "reasoning_content",
327
+ };
328
+ delete model.compat.thinkingFormat;
329
+ }
319
330
  const parsedModel = parseKnownModel(model.id);
320
331
  const applyPatchToolType = inferGeneratedApplyPatchToolType(model, parsedModel);
321
332
  if (applyPatchToolType) {
@@ -392,11 +403,19 @@ function inferModelThinking<TApi extends Api>(model: ApiModel<TApi>): ThinkingCo
392
403
  if (!minLevel || !maxLevel) {
393
404
  throw new Error(`Model ${model.provider}/${model.id} resolved to an empty thinking range`);
394
405
  }
395
- return {
406
+ const config: ThinkingConfig = {
396
407
  mode: inferThinkingControlMode(model, parsedModel),
397
408
  minLevel,
398
409
  maxLevel,
399
410
  };
411
+ // Encode explicit levels only when the inferred set has gaps the min..max range cannot represent.
412
+ const minIndex = THINKING_EFFORTS.indexOf(minLevel);
413
+ const maxIndex = THINKING_EFFORTS.indexOf(maxLevel);
414
+ const expandedRange = THINKING_EFFORTS.slice(minIndex, maxIndex + 1);
415
+ if (expandedRange.length !== efforts.length) {
416
+ config.levels = efforts;
417
+ }
418
+ return config;
400
419
  }
401
420
 
402
421
  function normalizeThinkingConfig(thinking: ThinkingConfig | undefined): ThinkingConfig | undefined {
@@ -409,10 +428,19 @@ function normalizeThinkingConfig(thinking: ThinkingConfig | undefined): Thinking
409
428
  function thinkingsEqual(left: ThinkingConfig | undefined, right: ThinkingConfig | undefined): boolean {
410
429
  if (left === right) return true;
411
430
  if (!left || !right) return false;
412
- return left.mode === right.mode && left.minLevel === right.minLevel && left.maxLevel === right.maxLevel;
431
+ if (left.mode !== right.mode || left.minLevel !== right.minLevel || left.maxLevel !== right.maxLevel) return false;
432
+ const leftLevels = left.levels;
433
+ const rightLevels = right.levels;
434
+ if (leftLevels === rightLevels) return true;
435
+ if (!leftLevels || !rightLevels) return false;
436
+ if (leftLevels.length !== rightLevels.length) return false;
437
+ return leftLevels.every((level, index) => level === rightLevels[index]);
413
438
  }
414
439
 
415
440
  function expandEffortRange(thinking: ThinkingConfig): readonly Effort[] {
441
+ if (thinking.levels && thinking.levels.length > 0) {
442
+ return thinking.levels;
443
+ }
416
444
  const minIndex = THINKING_EFFORTS.indexOf(thinking.minLevel);
417
445
  const maxIndex = THINKING_EFFORTS.indexOf(thinking.maxLevel);
418
446
  if (minIndex === -1 || maxIndex === -1 || minIndex > maxIndex) {
@@ -421,10 +449,6 @@ function expandEffortRange(thinking: ThinkingConfig): readonly Effort[] {
421
449
  return THINKING_EFFORTS.slice(minIndex, maxIndex + 1);
422
450
  }
423
451
 
424
- function intersectEfforts(left: readonly Effort[], right: readonly Effort[]): readonly Effort[] {
425
- return left.filter(effort => right.includes(effort));
426
- }
427
-
428
452
  function inferSupportedEfforts<TApi extends Api>(parsedModel: ParsedModel, model: ApiModel<TApi>): readonly Effort[] {
429
453
  switch (parsedModel.family) {
430
454
  case "openai":
package/src/models.json CHANGED
@@ -18451,7 +18451,7 @@
18451
18451
  "compat": {
18452
18452
  "supportsStore": false,
18453
18453
  "supportsDeveloperRole": false,
18454
- "thinkingFormat": "zai",
18454
+ "supportsReasoningEffort": false,
18455
18455
  "reasoningContentField": "reasoning_content"
18456
18456
  },
18457
18457
  "thinking": {
@@ -18481,7 +18481,7 @@
18481
18481
  "compat": {
18482
18482
  "supportsStore": false,
18483
18483
  "supportsDeveloperRole": false,
18484
- "thinkingFormat": "zai",
18484
+ "supportsReasoningEffort": false,
18485
18485
  "reasoningContentField": "reasoning_content"
18486
18486
  },
18487
18487
  "thinking": {
@@ -18508,7 +18508,7 @@
18508
18508
  },
18509
18509
  "compat": {
18510
18510
  "supportsDeveloperRole": false,
18511
- "thinkingFormat": "zai",
18511
+ "supportsReasoningEffort": false,
18512
18512
  "reasoningContentField": "reasoning_content"
18513
18513
  },
18514
18514
  "contextWindow": 1000000,
@@ -18540,7 +18540,7 @@
18540
18540
  "compat": {
18541
18541
  "supportsStore": false,
18542
18542
  "supportsDeveloperRole": false,
18543
- "thinkingFormat": "zai",
18543
+ "supportsReasoningEffort": false,
18544
18544
  "reasoningContentField": "reasoning_content"
18545
18545
  },
18546
18546
  "thinking": {
@@ -18570,7 +18570,7 @@
18570
18570
  "compat": {
18571
18571
  "supportsStore": false,
18572
18572
  "supportsDeveloperRole": false,
18573
- "thinkingFormat": "zai",
18573
+ "supportsReasoningEffort": false,
18574
18574
  "reasoningContentField": "reasoning_content"
18575
18575
  },
18576
18576
  "thinking": {
@@ -18597,7 +18597,7 @@
18597
18597
  },
18598
18598
  "compat": {
18599
18599
  "supportsDeveloperRole": false,
18600
- "thinkingFormat": "zai",
18600
+ "supportsReasoningEffort": false,
18601
18601
  "reasoningContentField": "reasoning_content"
18602
18602
  },
18603
18603
  "contextWindow": 204800,
@@ -18629,7 +18629,7 @@
18629
18629
  "compat": {
18630
18630
  "supportsStore": false,
18631
18631
  "supportsDeveloperRole": false,
18632
- "thinkingFormat": "zai",
18632
+ "supportsReasoningEffort": false,
18633
18633
  "reasoningContentField": "reasoning_content"
18634
18634
  },
18635
18635
  "thinking": {
@@ -18659,7 +18659,7 @@
18659
18659
  "compat": {
18660
18660
  "supportsStore": false,
18661
18661
  "supportsDeveloperRole": false,
18662
- "thinkingFormat": "zai",
18662
+ "supportsReasoningEffort": false,
18663
18663
  "reasoningContentField": "reasoning_content"
18664
18664
  },
18665
18665
  "thinking": {
@@ -18691,7 +18691,7 @@
18691
18691
  "compat": {
18692
18692
  "supportsStore": false,
18693
18693
  "supportsDeveloperRole": false,
18694
- "thinkingFormat": "zai",
18694
+ "supportsReasoningEffort": false,
18695
18695
  "reasoningContentField": "reasoning_content"
18696
18696
  },
18697
18697
  "thinking": {
@@ -18721,7 +18721,7 @@
18721
18721
  "compat": {
18722
18722
  "supportsStore": false,
18723
18723
  "supportsDeveloperRole": false,
18724
- "thinkingFormat": "zai",
18724
+ "supportsReasoningEffort": false,
18725
18725
  "reasoningContentField": "reasoning_content"
18726
18726
  },
18727
18727
  "thinking": {
@@ -18748,7 +18748,7 @@
18748
18748
  },
18749
18749
  "compat": {
18750
18750
  "supportsDeveloperRole": false,
18751
- "thinkingFormat": "zai",
18751
+ "supportsReasoningEffort": false,
18752
18752
  "reasoningContentField": "reasoning_content"
18753
18753
  },
18754
18754
  "contextWindow": 1000000,
@@ -18780,7 +18780,7 @@
18780
18780
  "compat": {
18781
18781
  "supportsStore": false,
18782
18782
  "supportsDeveloperRole": false,
18783
- "thinkingFormat": "zai",
18783
+ "supportsReasoningEffort": false,
18784
18784
  "reasoningContentField": "reasoning_content"
18785
18785
  },
18786
18786
  "thinking": {
@@ -18810,7 +18810,7 @@
18810
18810
  "compat": {
18811
18811
  "supportsStore": false,
18812
18812
  "supportsDeveloperRole": false,
18813
- "thinkingFormat": "zai",
18813
+ "supportsReasoningEffort": false,
18814
18814
  "reasoningContentField": "reasoning_content"
18815
18815
  },
18816
18816
  "thinking": {
@@ -18837,7 +18837,7 @@
18837
18837
  },
18838
18838
  "compat": {
18839
18839
  "supportsDeveloperRole": false,
18840
- "thinkingFormat": "zai",
18840
+ "supportsReasoningEffort": false,
18841
18841
  "reasoningContentField": "reasoning_content"
18842
18842
  },
18843
18843
  "contextWindow": 204800,
@@ -18869,7 +18869,7 @@
18869
18869
  "compat": {
18870
18870
  "supportsStore": false,
18871
18871
  "supportsDeveloperRole": false,
18872
- "thinkingFormat": "zai",
18872
+ "supportsReasoningEffort": false,
18873
18873
  "reasoningContentField": "reasoning_content"
18874
18874
  },
18875
18875
  "thinking": {
@@ -18899,7 +18899,7 @@
18899
18899
  "compat": {
18900
18900
  "supportsStore": false,
18901
18901
  "supportsDeveloperRole": false,
18902
- "thinkingFormat": "zai",
18902
+ "supportsReasoningEffort": false,
18903
18903
  "reasoningContentField": "reasoning_content"
18904
18904
  },
18905
18905
  "thinking": {
@@ -2112,7 +2112,7 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_CODING_PLANS: readonly ModelsDevProviderDe
2112
2112
  compat: {
2113
2113
  supportsStore: false,
2114
2114
  supportsDeveloperRole: false,
2115
- thinkingFormat: "zai",
2115
+ supportsReasoningEffort: false,
2116
2116
  reasoningContentField: "reasoning_content",
2117
2117
  },
2118
2118
  }),
@@ -2120,7 +2120,7 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_CODING_PLANS: readonly ModelsDevProviderDe
2120
2120
  compat: {
2121
2121
  supportsStore: false,
2122
2122
  supportsDeveloperRole: false,
2123
- thinkingFormat: "zai",
2123
+ supportsReasoningEffort: false,
2124
2124
  reasoningContentField: "reasoning_content",
2125
2125
  },
2126
2126
  }),
@@ -2438,7 +2438,7 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
2438
2438
  }
2439
2439
  if (block.type === "toolCall") {
2440
2440
  const toolCall = block as ToolCall;
2441
- const normalized = normalizeResponsesToolCallId(toolCall.id);
2441
+ const normalized = normalizeResponsesToolCallId(toolCall.id, toolCall.customWireName ? "ctc" : "fc");
2442
2442
  if (toolCall.customWireName) {
2443
2443
  const rawInput = typeof toolCall.arguments?.input === "string" ? toolCall.arguments.input : "";
2444
2444
  customCallIds.add(normalized.callId);
@@ -287,10 +287,18 @@ function getTrailingPartialTag(text: string, tags: readonly string[]): string {
287
287
  // Body is restricted to identifier-like chars (with the DeepSeek tokenizer's `▁`),
288
288
  // capped at a sane length to avoid swallowing legitimate angle-bracket text.
289
289
  const DEEPSEEK_SPECIAL_TOKEN_REGEX = /<(?:||\|)[A-Za-z0-9_.||▁]{1,64}(?:||\|)>/g;
290
+ const DEEPSEEK_SPECIAL_TOKEN_AT_START_REGEX = /^\s*<(?:||\|)[A-Za-z0-9_.||▁]{1,64}(?:||\|)>/;
291
+ const DEEPSEEK_SPECIAL_TOKEN_AT_END_REGEX = /<(?:||\|)[A-Za-z0-9_.||▁]{1,64}(?:||\|)>\s*$/;
290
292
  const DEEPSEEK_OPEN_DELIMS = ["<|", "<|"] as const;
291
293
 
292
294
  function stripDeepseekSpecialTokens(text: string): string {
293
- return text.replace(DEEPSEEK_SPECIAL_TOKEN_REGEX, "");
295
+ const stripped = text.replace(DEEPSEEK_SPECIAL_TOKEN_REGEX, "");
296
+ if (stripped === text) return text;
297
+
298
+ let normalized = stripped;
299
+ if (DEEPSEEK_SPECIAL_TOKEN_AT_START_REGEX.test(text)) normalized = normalized.replace(/^\s+/u, "");
300
+ if (DEEPSEEK_SPECIAL_TOKEN_AT_END_REGEX.test(text)) normalized = normalized.replace(/\s+$/u, "");
301
+ return normalized;
294
302
  }
295
303
 
296
304
  // Find any trailing partial `<|...` (or `<|...`) that has not yet been closed by a
@@ -431,10 +439,12 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
431
439
  stream.push({ type: "start", partial: output });
432
440
 
433
441
  const parseMiniMaxThinkTags = model.provider === "minimax-code";
434
- // NVIDIA NIM and similar OpenAI-compatible hosts return DeepSeek's chat-template
435
- // tool-call markers in `delta.content` even though tool calls are also surfaced
436
- // structurally. Strip the leaked markers so users don't see raw `<|...|>` tokens.
437
- const stripDeepseekChatTemplateTokens = model.provider === "nvidia" && /deepseek/i.test(model.id);
442
+ // Some OpenAI-compatible DeepSeek hosts (including NVIDIA NIM and DeepSeek's
443
+ // native API) leak chat-template tool-call markers in `delta.content` even
444
+ // though tool calls are also surfaced structurally. Strip the leaked markers
445
+ // so users don't see raw `<|...|>` tokens.
446
+ const stripDeepseekChatTemplateTokens =
447
+ /deepseek/i.test(model.id) && (model.provider === "nvidia" || model.provider === "deepseek");
438
448
  type OpenAIStreamBlock = TextContent | ThinkingContent | (ToolCall & { partialArgs: string });
439
449
  let currentBlock: OpenAIStreamBlock | undefined;
440
450
  const blockIndex = (block: OpenAIStreamBlock | undefined): number => {
@@ -568,7 +578,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
568
578
  deepseekStripBuffer = trailing;
569
579
  }
570
580
  const stripped = stripDeepseekSpecialTokens(flushable);
571
- if (stripped) appendTextDelta(stripped);
581
+ if (stripped && (stripped === flushable || stripped.trim().length > 0)) appendTextDelta(stripped);
572
582
  };
573
583
 
574
584
  for await (const chunk of iterateWithIdleTimeout(openaiStream, {
@@ -187,9 +187,9 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
187
187
  continue;
188
188
  }
189
189
 
190
- const normalized = normalizeResponsesToolCallId(block.id);
190
+ const normalized = normalizeResponsesToolCallId(block.id, block.customWireName ? "ctc" : "fc");
191
191
  let itemId: string | undefined = normalized.itemId;
192
- if (isDifferentModel && (itemId?.startsWith("fc_") || itemId?.startsWith("fcr_"))) {
192
+ if (isDifferentModel && (itemId?.startsWith("fc_") || itemId?.startsWith("fcr_") || itemId?.startsWith("ctc_"))) {
193
193
  itemId = undefined;
194
194
  }
195
195
  knownCallIds.add(normalized.callId);
package/src/types.ts CHANGED
@@ -83,6 +83,12 @@ export interface ThinkingConfig {
83
83
  minLevel: Effort;
84
84
  /** Most intensive supported user-facing effort level. */
85
85
  maxLevel: Effort;
86
+ /**
87
+ * Optional explicit list of supported levels. When present, takes precedence over
88
+ * the `minLevel`..`maxLevel` range — used to encode discrete sets with gaps
89
+ * (e.g. Gemini 3 Pro supports `low` and `high` but not `medium`).
90
+ */
91
+ levels?: readonly Effort[];
86
92
  /** Optional default effort applied when this model is selected. Falls back to global default if absent. */
87
93
  defaultLevel?: Effort;
88
94
  /** Provider-specific transport used to encode the selected effort. */
@@ -15,6 +15,7 @@ const DEFAULT_OAUTH_HOST = "https://auth.kimi.com";
15
15
  const DEVICE_ID_FILENAME = "kimi-device-id";
16
16
  const DEFAULT_POLL_INTERVAL_MS = 5000;
17
17
  const DEFAULT_DEVICE_FLOW_TTL_MS = 15 * 60 * 1000;
18
+ const OAUTH_EXPIRY_SKEW_MS = 5 * 60 * 1000;
18
19
 
19
20
  interface DeviceAuthorizationResponse {
20
21
  user_code?: string;
@@ -146,7 +147,7 @@ function parseTokenPayload(payload: TokenResponse, refreshTokenFallback?: string
146
147
  return {
147
148
  access: payload.access_token,
148
149
  refresh,
149
- expires: Date.now() + payload.expires_in * 1000,
150
+ expires: Date.now() + payload.expires_in * 1000 - OAUTH_EXPIRY_SKEW_MS,
150
151
  };
151
152
  }
152
153
 
package/src/utils.ts CHANGED
@@ -5,8 +5,10 @@ import type { CacheRetention, OpenAIResponsesHistoryPayload, ProviderPayload } f
5
5
  type OpenAIResponsesReplayItem = ResponseInput[number];
6
6
 
7
7
  export { isRecord } from "@oh-my-pi/pi-utils";
8
- export function normalizeSystemPrompts(systemPrompt: readonly string[] | undefined): string[] {
9
- return systemPrompt?.map(prompt => prompt.toWellFormed()).filter(prompt => prompt.length > 0) ?? [];
8
+ export function normalizeSystemPrompts(systemPrompt: readonly string[] | string | undefined | null): string[] {
9
+ if (systemPrompt === undefined || systemPrompt === null) return [];
10
+ const prompts = Array.isArray(systemPrompt) ? systemPrompt : typeof systemPrompt === "string" ? [systemPrompt] : [];
11
+ return prompts.map(prompt => prompt.toWellFormed()).filter(prompt => prompt.length > 0);
10
12
  }
11
13
 
12
14
  export function toNumber(value: unknown): number | undefined {
@@ -34,16 +36,21 @@ export function normalizeToolCallId(id: string): string {
34
36
  return sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized;
35
37
  }
36
38
 
37
- export function normalizeResponsesToolCallId(id: string): { callId: string; itemId: string } {
39
+ type ResponsesToolItemIdPrefix = "fc" | "ctc";
40
+
41
+ export function normalizeResponsesToolCallId(
42
+ id: string,
43
+ itemPrefix: ResponsesToolItemIdPrefix = "fc",
44
+ ): { callId: string; itemId: string } {
38
45
  const [callId, itemId] = id.split("|");
39
46
  if (callId && itemId) {
40
47
  const normalizedCallId = truncateResponseItemId(callId, getIdPrefix(callId, "call"));
41
- const normalizedItemId = normalizeResponsesItemId(itemId);
48
+ const normalizedItemId = normalizeResponsesItemId(itemId, itemPrefix);
42
49
  return { callId: normalizedCallId, itemId: normalizedItemId };
43
50
  }
44
51
  const hash = Bun.hash(id).toString(36);
45
52
  const normalizedCallId = id.startsWith("call_") ? truncateResponseItemId(id, "call") : `call_${hash}`;
46
- return { callId: normalizedCallId, itemId: `fc_${hash}` };
53
+ return { callId: normalizedCallId, itemId: `${itemPrefix}_${hash}` };
47
54
  }
48
55
 
49
56
  function getIdPrefix(id: string, fallback: string): string {
@@ -51,10 +58,19 @@ function getIdPrefix(id: string, fallback: string): string {
51
58
  return prefix || fallback;
52
59
  }
53
60
 
54
- function normalizeResponsesItemId(itemId: string): string {
55
- const prefix = getIdPrefix(itemId, "fc");
56
- if (prefix !== "fc" && prefix !== "fcr") {
57
- return `fc_${Bun.hash(itemId).toString(36)}`;
61
+ function getExplicitIdPrefix(id: string): string | undefined {
62
+ return id.match(/^([a-zA-Z][a-zA-Z0-9]*)_/)?.[1];
63
+ }
64
+
65
+ function normalizeResponsesItemId(itemId: string, fallbackPrefix: ResponsesToolItemIdPrefix): string {
66
+ const prefix = getExplicitIdPrefix(itemId);
67
+ const isAllowedPrefix = prefix
68
+ ? fallbackPrefix === "ctc"
69
+ ? prefix === "ctc"
70
+ : prefix === "fc" || prefix === "fcr"
71
+ : false;
72
+ if (!prefix || !isAllowedPrefix) {
73
+ return `${fallbackPrefix}_${Bun.hash(itemId).toString(36)}`;
58
74
  }
59
75
  return truncateResponseItemId(itemId, prefix);
60
76
  }