@gajae-code/ai 0.17.4 → 0.17.6

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.
@@ -230,6 +230,13 @@ const STATIC_KIRO_API_CATALOG: Array<{
230
230
  maxOutputTokens: 128_000,
231
231
  image: true,
232
232
  },
233
+ {
234
+ modelId: "claude-opus-5.5",
235
+ modelName: "Claude Opus 5.5",
236
+ maxInputTokens: 1_000_000,
237
+ maxOutputTokens: 128_000,
238
+ image: false,
239
+ },
233
240
  {
234
241
  modelId: "gpt-5.6-luna",
235
242
  modelName: "GPT 5.6 Luna",
@@ -8,6 +8,7 @@ import type {
8
8
  ChatCompletionContentPartImage,
9
9
  ChatCompletionContentPartText,
10
10
  ChatCompletionMessageParam,
11
+ ChatCompletionToolChoiceOption,
11
12
  ChatCompletionToolMessageParam,
12
13
  } from "openai/resources/chat/completions";
13
14
  import packageJson from "../../package.json" with { type: "json" };
@@ -1884,25 +1885,6 @@ function buildParams(
1884
1885
  params.reasoning_effort = mapReasoningEffort(minEffort, compat.reasoningEffortMap) as Effort;
1885
1886
  }
1886
1887
 
1887
- if (compat.disableReasoningOnToolChoice && params.tool_choice !== undefined) {
1888
- // DeepSeek reasoning models accept tools/tool_choice, but reject that
1889
- // control field while thinking is enabled. Keep the tool-selection
1890
- // contract and suppress reasoning for this single request.
1891
- delete params.reasoning_effort;
1892
- delete params.reasoning;
1893
- }
1894
-
1895
- if (compat.disableReasoningOnForcedToolChoice && isForcedToolChoice(params.tool_choice)) {
1896
- // Backends like Kimi 400 with `tool_choice 'specified' is incompatible
1897
- // with thinking enabled`. Suppress thinking for this single forced-tool
1898
- // turn while keeping the tool-selection contract intact.
1899
- delete params.reasoning_effort;
1900
- delete params.reasoning;
1901
- if (compat.thinkingFormat === "zai") {
1902
- params.thinking = { type: "disabled" };
1903
- }
1904
- }
1905
-
1906
1888
  // OpenRouter provider routing preferences
1907
1889
  if (model.baseUrl.includes("openrouter.ai") && compat.openRouterRouting) {
1908
1890
  params.provider = compat.openRouterRouting;
@@ -1922,13 +1904,46 @@ function buildParams(
1922
1904
  if (compat.extraBody) {
1923
1905
  // The resolved output limit owns the selected wire field; extraBody is a
1924
1906
  // free-form compatibility escape hatch and must not add a competing
1925
- // max-token field or overwrite the resolved budget.
1926
- const { max_tokens, max_completion_tokens, max_output_tokens, ...restExtra } = compat.extraBody as Record<
1927
- string,
1928
- unknown
1929
- >;
1907
+ // max-token field or overwrite the resolved budget. tool_choice follows
1908
+ // the same discipline on both sides: an injected default (an endpoint
1909
+ // whose tool_choice default is "none", like IO Intelligence, would
1910
+ // otherwise stop tool calls) may only fill the gap on an ordinary turn
1911
+ // that offers tools but resolved no directive of its own. Explicit
1912
+ // directives — forced tools, retry reminders — stay untouched, and
1913
+ // turns that deliberately carry no tools keep their stripped shape
1914
+ // instead of re-adding tool_choice with an empty tools list.
1915
+ const { max_tokens, max_completion_tokens, max_output_tokens, tool_choice, ...restExtra } =
1916
+ compat.extraBody as Record<string, unknown>;
1917
+ if (
1918
+ tool_choice !== undefined &&
1919
+ params.tool_choice === undefined &&
1920
+ Array.isArray(params.tools) &&
1921
+ params.tools.length > 0
1922
+ ) {
1923
+ params.tool_choice = tool_choice as ChatCompletionToolChoiceOption;
1924
+ }
1930
1925
  Object.assign(params, restExtra);
1931
1926
  }
1927
+
1928
+ if (compat.disableReasoningOnToolChoice && params.tool_choice !== undefined) {
1929
+ // DeepSeek reasoning models accept tools/tool_choice, but reject that
1930
+ // control field while thinking is enabled. Keep the tool-selection
1931
+ // contract and suppress reasoning for this single request.
1932
+ delete params.reasoning_effort;
1933
+ delete params.reasoning;
1934
+ }
1935
+
1936
+ if (compat.disableReasoningOnForcedToolChoice && isForcedToolChoice(params.tool_choice)) {
1937
+ // Backends like Kimi 400 with `tool_choice 'specified' is incompatible
1938
+ // with thinking enabled`. Suppress thinking for this single forced-tool
1939
+ // turn while keeping the tool-selection contract intact.
1940
+ delete params.reasoning_effort;
1941
+ delete params.reasoning;
1942
+ if (compat.thinkingFormat === "zai") {
1943
+ params.thinking = { type: "disabled" };
1944
+ }
1945
+ }
1946
+
1932
1947
  applyOpenAIRequestTransformBody(params, model.requestTransform);
1933
1948
  if (!supportsReasoningParams) {
1934
1949
  delete params.reasoning;
@@ -85,6 +85,59 @@ function collapseAdjacentThinking<T extends { type: string }>(content: T[]): T[]
85
85
  return dropped ? collapsed : content;
86
86
  }
87
87
 
88
+ const MIN_CROSS_MODEL_THINKING_REPEAT_COUNT = 64;
89
+ const MIN_CROSS_MODEL_THINKING_REPEAT_SAVED_CHARACTERS = 4_096;
90
+
91
+ /**
92
+ * Bound pathological cross-model reasoning replay without editing the stored
93
+ * thinking block. Only exact adjacent non-empty paragraphs qualify, and the
94
+ * threshold requires both a large run and substantial net savings.
95
+ */
96
+ function compressRepeatedThinkingParagraphs(thinking: string): string {
97
+ const parts = thinking.split(/(\r?\n(?:[ \t]*\r?\n)+)/);
98
+ const compressedParts: string[] = [];
99
+ let compressed = false;
100
+
101
+ for (let paragraphIndex = 0; paragraphIndex < parts.length; ) {
102
+ const paragraph = parts[paragraphIndex];
103
+ let runEnd = paragraphIndex;
104
+ while (runEnd + 2 < parts.length && parts[runEnd + 2] === paragraph) {
105
+ runEnd += 2;
106
+ }
107
+
108
+ const repeatCount = (runEnd - paragraphIndex) / 2 + 1;
109
+ let marker: string | undefined;
110
+ let savedCharacters = 0;
111
+ if (paragraph.length > 0 && repeatCount >= MIN_CROSS_MODEL_THINKING_REPEAT_COUNT) {
112
+ marker = `[Repeated paragraph occurred exactly ${repeatCount} consecutive times; only its first occurrence is shown.]`;
113
+ savedCharacters = (repeatCount - 1) * paragraph.length - marker.length;
114
+ for (let separatorIndex = paragraphIndex + 3; separatorIndex < runEnd; separatorIndex += 2) {
115
+ savedCharacters += parts[separatorIndex].length;
116
+ }
117
+ }
118
+
119
+ if (marker !== undefined && savedCharacters >= MIN_CROSS_MODEL_THINKING_REPEAT_SAVED_CHARACTERS) {
120
+ compressedParts.push(paragraph);
121
+ if (paragraphIndex + 1 < parts.length) compressedParts.push(parts[paragraphIndex + 1]);
122
+ compressedParts.push(marker);
123
+ compressed = true;
124
+ } else {
125
+ for (let partIndex = paragraphIndex; partIndex <= runEnd; partIndex++) {
126
+ compressedParts.push(parts[partIndex]);
127
+ }
128
+ }
129
+
130
+ // The separator after the run belongs to the next paragraph and must
131
+ // survive verbatim, regardless of whether this run was compressed.
132
+ if (runEnd + 1 < parts.length) {
133
+ compressedParts.push(parts[runEnd + 1]);
134
+ }
135
+ paragraphIndex = runEnd + 2;
136
+ }
137
+
138
+ return compressed ? compressedParts.join("") : thinking;
139
+ }
140
+
88
141
  export function transformMessages<TApi extends Api>(
89
142
  messages: Message[],
90
143
  model: Model<TApi>,
@@ -170,7 +223,7 @@ export function transformMessages<TApi extends Api>(
170
223
  if (isSameModel) return sanitized;
171
224
  return {
172
225
  type: "text" as const,
173
- text: sanitized.thinking,
226
+ text: compressRepeatedThinkingParagraphs(sanitized.thinking),
174
227
  };
175
228
  }
176
229