@deepstrike/sdk 0.2.6 → 0.2.8

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 (40) hide show
  1. package/dist/index.d.ts +8 -4
  2. package/dist/index.js +5 -2
  3. package/dist/kernel.d.ts +54 -0
  4. package/dist/kernel.js +8 -0
  5. package/dist/providers/anthropic.d.ts +4 -1
  6. package/dist/providers/anthropic.js +51 -0
  7. package/dist/providers/base.d.ts +6 -0
  8. package/dist/providers/base.js +10 -1
  9. package/dist/providers/catalog.js +5 -2
  10. package/dist/providers/deepseek.d.ts +4 -1
  11. package/dist/providers/deepseek.js +79 -8
  12. package/dist/providers/glm.d.ts +2 -1
  13. package/dist/providers/glm.js +7 -0
  14. package/dist/providers/kimi.d.ts +2 -1
  15. package/dist/providers/kimi.js +7 -0
  16. package/dist/providers/minimax.d.ts +28 -2
  17. package/dist/providers/minimax.js +200 -1
  18. package/dist/providers/openai-chat.d.ts +18 -2
  19. package/dist/providers/openai-chat.js +37 -3
  20. package/dist/providers/openai.d.ts +14 -1
  21. package/dist/providers/openai.js +48 -7
  22. package/dist/providers/profiles.d.ts +6 -0
  23. package/dist/providers/profiles.js +6 -0
  24. package/dist/providers/qwen.d.ts +3 -1
  25. package/dist/providers/qwen.js +20 -2
  26. package/dist/providers/replay-validator.d.ts +32 -0
  27. package/dist/providers/replay-validator.js +90 -0
  28. package/dist/runtime/kernel-event-log.js +22 -0
  29. package/dist/runtime/kernel-step.d.ts +13 -0
  30. package/dist/runtime/provider-replay.d.ts +16 -1
  31. package/dist/runtime/provider-replay.js +47 -4
  32. package/dist/runtime/runner.d.ts +22 -1
  33. package/dist/runtime/runner.js +68 -2
  34. package/dist/runtime/session-log.d.ts +22 -0
  35. package/dist/runtime/session-repair.d.ts +26 -3
  36. package/dist/runtime/session-repair.js +33 -32
  37. package/dist/types/agent.d.ts +50 -0
  38. package/dist/types/agent.js +110 -0
  39. package/dist/types.d.ts +38 -0
  40. package/package.json +2 -2
@@ -1,5 +1,7 @@
1
1
  import { AnthropicProvider } from "./anthropic.js";
2
+ import { OpenAIChatProvider } from "./openai.js";
2
3
  import { endpointProfiles } from "./profiles.js";
4
+ import { omitExtensionKeys } from "./base.js";
3
5
  const MINIMAX_POLICIES = {
4
6
  "MiniMax-M2.7": { maxTurns: 35 },
5
7
  "MiniMax-M2.7-highspeed": { maxTurns: 35 },
@@ -10,14 +12,211 @@ const MINIMAX_POLICIES = {
10
12
  "MiniMax-M2": { maxTurns: 20 },
11
13
  "MiniMax-Text-01": { maxTurns: 20 },
12
14
  };
13
- export class MiniMaxProvider extends AnthropicProvider {
15
+ /**
16
+ * MiniMax over its Anthropic-compatible endpoint. Replay is carried as Anthropic
17
+ * `native_blocks` (thinking/text/tool_use), identical to the first-party
18
+ * Anthropic provider.
19
+ */
20
+ export class MiniMaxAnthropicProvider extends AnthropicProvider {
14
21
  constructor(apiKey, model = "MiniMax-M2.7", retry, baseURL = endpointProfiles["minimax.anthropic"].baseURL) {
15
22
  super(apiKey, model, retry, {
16
23
  baseURL,
17
24
  authMode: "api-key",
18
25
  });
19
26
  }
27
+ providerName() {
28
+ return "minimax";
29
+ }
30
+ runtimePolicy() {
31
+ return MINIMAX_POLICIES[this.model] ?? {};
32
+ }
33
+ }
34
+ /**
35
+ * MiniMax over its OpenAI-compatible endpoint. Replay is carried as
36
+ * `reasoning_content` / `reasoning_details` (split reasoning), and requests
37
+ * default to `reasoning_split: true` so reasoning is returned out-of-band rather
38
+ * than embedded in the message content.
39
+ */
40
+ export class MiniMaxOpenAIProvider extends OpenAIChatProvider {
41
+ constructor(apiKey, model = "MiniMax-M2.7", retry, baseURL = endpointProfiles["minimax.openai"].baseURL) {
42
+ super(apiKey, model, retry, baseURL);
43
+ }
20
44
  runtimePolicy() {
21
45
  return MINIMAX_POLICIES[this.model] ?? {};
22
46
  }
47
+ descriptor() {
48
+ return {
49
+ provider: "minimax",
50
+ protocol: "openai-chat",
51
+ model: this.model,
52
+ reasoning: {
53
+ supported: true,
54
+ preserveAcrossToolTurns: true,
55
+ requiresReplayForToolTurns: true,
56
+ },
57
+ toolCalls: {
58
+ supported: true,
59
+ requiresStrictPairing: true,
60
+ },
61
+ };
62
+ }
63
+ requireNonEmptyReasoningReplayForToolTurns(extensions) {
64
+ if (extensions?.__deepstrikeThinkingEnabled === false)
65
+ return false;
66
+ return extensions?.reasoning_split !== false;
67
+ }
68
+ buildRequestExtensions(extensions) {
69
+ const reasoningSplit = extensions?.reasoning_split !== false;
70
+ return {
71
+ ...omitExtensionKeys(extensions, ["reasoning_split", "exposeReasoning"]),
72
+ __deepstrikeThinkingEnabled: reasoningSplit,
73
+ // Re-thread the degrade control flag (omitExtensionKeys strips internal
74
+ // keys) so buildChatMessages can honor it; the wire-request omit drops it.
75
+ ...(extensions?.degradeMissingReasoningReplay === true ? { degradeMissingReasoningReplay: true } : {}),
76
+ reasoning_split: reasoningSplit,
77
+ };
78
+ }
79
+ async complete(context, tools, extensions) {
80
+ const requestExtensions = this.buildRequestExtensions(extensions);
81
+ if (this.circuit.isOpen())
82
+ throw new Error("Circuit breaker open");
83
+ const msgs = this.buildChatMessages(context, requestExtensions);
84
+ let lastErr;
85
+ for (let i = 0; i < this.maxRetries; i++) {
86
+ try {
87
+ const resp = await this.client.chat.completions.create({
88
+ ...this.requestExtensions(requestExtensions),
89
+ model: this.model,
90
+ messages: msgs,
91
+ ...(tools.length ? { tools: this.chat.buildTools(tools) } : {}),
92
+ });
93
+ this.circuit.recordSuccess();
94
+ const choice = resp.choices[0].message;
95
+ const nativeToolCalls = choice.tool_calls ?? [];
96
+ const toolCalls = this.chat.normalizeToolCalls(nativeToolCalls);
97
+ const content = choice.content ?? "";
98
+ this.rememberMiniMaxReplay(content, toolCalls, choice.reasoning_content, choice.reasoning_details, nativeToolCalls);
99
+ return { role: "assistant", content, tokenCount: resp.usage?.completion_tokens ?? resp.usage?.total_tokens, toolCalls };
100
+ }
101
+ catch (err) {
102
+ lastErr = err;
103
+ this.circuit.recordFailure();
104
+ if (i < this.maxRetries - 1)
105
+ await new Promise(r => setTimeout(r, this.baseDelay * 2 ** i));
106
+ }
107
+ }
108
+ throw lastErr;
109
+ }
110
+ async *stream(context, tools, extensions) {
111
+ const exposeReasoning = extensions?.exposeReasoning ?? false;
112
+ const requestExtensions = this.buildRequestExtensions(extensions);
113
+ const msgs = this.buildChatMessages(context, requestExtensions);
114
+ const toolCallBufs = {};
115
+ const emittedToolCallIndexes = new Set();
116
+ let reasoningContent = "";
117
+ let reasoningDetails;
118
+ let finalText = "";
119
+ const stream = await this.client.chat.completions.create({
120
+ ...this.requestExtensions(requestExtensions),
121
+ model: this.model,
122
+ messages: msgs,
123
+ ...(tools.length ? { tools: this.chat.buildTools(tools) } : {}),
124
+ stream: true,
125
+ stream_options: { include_usage: true },
126
+ });
127
+ let totalTokens = 0;
128
+ let inputTokens = 0;
129
+ let outputTokens = 0;
130
+ for await (const chunk of stream) {
131
+ if (chunk.usage) {
132
+ totalTokens = chunk.usage.total_tokens;
133
+ inputTokens = chunk.usage.prompt_tokens ?? 0;
134
+ outputTokens = chunk.usage.completion_tokens ?? 0;
135
+ continue;
136
+ }
137
+ const choice = chunk.choices[0];
138
+ if (!choice)
139
+ continue;
140
+ const delta = choice.delta;
141
+ if (!delta)
142
+ continue;
143
+ if (exposeReasoning && delta.reasoning_content) {
144
+ yield { type: "thinking_delta", delta: String(delta.reasoning_content) };
145
+ }
146
+ if (delta.reasoning_content)
147
+ reasoningContent += String(delta.reasoning_content);
148
+ if (delta.reasoning_details !== undefined && delta.reasoning_details !== null)
149
+ reasoningDetails = delta.reasoning_details;
150
+ if (delta.content) {
151
+ finalText += String(delta.content);
152
+ yield { type: "text_delta", delta: delta.content };
153
+ }
154
+ for (const tc of delta.tool_calls ?? []) {
155
+ const idx = tc.index;
156
+ if (!toolCallBufs[idx])
157
+ toolCallBufs[idx] = { id: tc.id ?? "", name: "", argsBuf: "" };
158
+ if (tc.function?.name)
159
+ toolCallBufs[idx].name += tc.function.name;
160
+ toolCallBufs[idx].argsBuf += tc.function?.arguments ?? "";
161
+ }
162
+ if (choice.finish_reason === "tool_calls") {
163
+ const toolCalls = Object.values(toolCallBufs).map(tb => ({ id: tb.id, name: tb.name, arguments: tb.argsBuf || "{}" }));
164
+ this.rememberMiniMaxReplay(finalText, toolCalls, reasoningContent, reasoningDetails, nativeToolCallsFromBuffers(toolCallBufs));
165
+ for (const [index, tb] of Object.entries(toolCallBufs)) {
166
+ const idx = Number(index);
167
+ if (emittedToolCallIndexes.has(idx))
168
+ continue;
169
+ let args = {};
170
+ try {
171
+ args = JSON.parse(tb.argsBuf || "{}");
172
+ }
173
+ catch {
174
+ args = {};
175
+ }
176
+ emittedToolCallIndexes.add(idx);
177
+ yield { type: "tool_call", id: tb.id, name: tb.name, arguments: args };
178
+ }
179
+ }
180
+ }
181
+ const toolCalls = Object.values(toolCallBufs).map(tb => ({ id: tb.id, name: tb.name, arguments: tb.argsBuf || "{}" }));
182
+ this.rememberMiniMaxReplay(finalText, toolCalls, reasoningContent, reasoningDetails, nativeToolCallsFromBuffers(toolCallBufs));
183
+ for (const [index, tb] of Object.entries(toolCallBufs)) {
184
+ const idx = Number(index);
185
+ if (emittedToolCallIndexes.has(idx))
186
+ continue;
187
+ let args = {};
188
+ try {
189
+ args = JSON.parse(tb.argsBuf || "{}");
190
+ }
191
+ catch {
192
+ args = {};
193
+ }
194
+ emittedToolCallIndexes.add(idx);
195
+ yield { type: "tool_call", id: tb.id, name: tb.name, arguments: args };
196
+ }
197
+ if (totalTokens > 0)
198
+ yield { type: "usage", totalTokens, inputTokens, outputTokens };
199
+ }
200
+ rememberMiniMaxReplay(content, toolCalls, reasoningContent, reasoningDetails, nativeToolCalls) {
201
+ const hasReasoning = typeof reasoningContent === "string" && reasoningContent.trim().length > 0;
202
+ const hasDetails = reasoningDetails !== undefined && reasoningDetails !== null;
203
+ if (!hasReasoning && !hasDetails)
204
+ return;
205
+ this.chat.rememberReplayFields({ content, toolCalls }, {
206
+ schema_version: 2,
207
+ provider: "minimax",
208
+ protocol: "openai-chat",
209
+ model: this.model,
210
+ ...(hasReasoning ? { reasoning_content: reasoningContent } : {}),
211
+ ...(hasDetails ? { reasoning_details: reasoningDetails } : {}),
212
+ ...(nativeToolCalls.length ? { tool_calls: nativeToolCalls } : {}),
213
+ });
214
+ }
215
+ }
216
+ function nativeToolCallsFromBuffers(toolCallBufs) {
217
+ return Object.values(toolCallBufs).map(tb => ({
218
+ id: tb.id,
219
+ type: "function",
220
+ function: { name: tb.name, arguments: tb.argsBuf || "{}" },
221
+ }));
23
222
  }
@@ -1,5 +1,16 @@
1
1
  import type OpenAI from "openai";
2
- import type { Message, RenderedContext, ToolSchema } from "../types.js";
2
+ import type { Message, ProviderDescriptor, RenderedContext, ToolSchema } from "../types.js";
3
+ import { type ReplayabilityAssessment } from "./replay-validator.js";
4
+ export interface OpenAIChatBuildMessageOptions {
5
+ descriptor?: ProviderDescriptor;
6
+ requireNonEmptyReasoningForToolCalls?: boolean;
7
+ /**
8
+ * Degrade (rather than throw) when a reasoning-requiring tool-call turn has
9
+ * no stored reasoning replay: a placeholder reasoning is injected so the
10
+ * request still goes out in degraded form.
11
+ */
12
+ degradeMissingReasoning?: boolean;
13
+ }
3
14
  export declare class OpenAIChatAdapter {
4
15
  private replayFields;
5
16
  buildTools(tools: ToolSchema[]): {
@@ -10,7 +21,12 @@ export declare class OpenAIChatAdapter {
10
21
  parameters: any;
11
22
  };
12
23
  }[];
13
- buildMessages(context: RenderedContext): OpenAI.ChatCompletionMessageParam[];
24
+ buildMessages(context: RenderedContext, options?: OpenAIChatBuildMessageOptions): OpenAI.ChatCompletionMessageParam[];
25
+ /**
26
+ * Throw-free pre-flight check: which assistant tool-call turns in `context`
27
+ * lack the non-empty reasoning replay a reasoning-requiring provider needs.
28
+ */
29
+ assessReasoning(context: RenderedContext): ReplayabilityAssessment;
14
30
  normalizeToolCalls(toolCalls?: OpenAI.ChatCompletionMessageToolCall[]): Array<{
15
31
  id: string;
16
32
  name: string;
@@ -1,5 +1,6 @@
1
1
  import { assistantReplayKey } from "../runtime/provider-replay.js";
2
2
  import { normalizeToolCall, toOpenAIMessageParams } from "./base.js";
3
+ import { DEGRADED_REASONING_PLACEHOLDER, assessReasoningReplay, validateOpenAIChatReplay, } from "./replay-validator.js";
3
4
  export class OpenAIChatAdapter {
4
5
  replayFields = new Map();
5
6
  buildTools(tools) {
@@ -8,7 +9,14 @@ export class OpenAIChatAdapter {
8
9
  function: { name: t.name, description: t.description, parameters: JSON.parse(t.parameters) },
9
10
  }));
10
11
  }
11
- buildMessages(context) {
12
+ buildMessages(context, options = {}) {
13
+ validateOpenAIChatReplay(context, {
14
+ descriptor: options.descriptor,
15
+ requireNonEmptyReasoningForToolCalls: options.requireNonEmptyReasoningForToolCalls,
16
+ degradeMissingReasoning: options.degradeMissingReasoning,
17
+ replayForAssistant: message => this.replayFields.get(assistantReplayKey(message)),
18
+ });
19
+ const degradeReasoning = Boolean(options.requireNonEmptyReasoningForToolCalls && options.degradeMissingReasoning);
12
20
  // toOpenAIMessageParams prepends systemText as messages[0], then turns.
13
21
  const serialized = toOpenAIMessageParams(context);
14
22
  // Cursor starts at 1 to skip the system message injected by toOpenAIMessageParams.
@@ -20,13 +28,29 @@ export class OpenAIChatAdapter {
20
28
  }
21
29
  if (source.role === "assistant") {
22
30
  const replay = this.replayFields.get(assistantReplayKey(source));
23
- if (replay)
24
- serialized[cursor] = { ...serialized[cursor], ...replay };
31
+ let wireReplay = openAIChatWireReplayFields(replay);
32
+ if (!wireReplay && degradeReasoning && source.toolCalls?.length) {
33
+ // Reasoning-requiring provider, no stored reasoning for this tool-call
34
+ // turn, caller opted into degradation: inject a placeholder so the
35
+ // wire message stays well-formed instead of failing the whole request.
36
+ wireReplay = { reasoning_content: DEGRADED_REASONING_PLACEHOLDER };
37
+ }
38
+ if (wireReplay)
39
+ serialized[cursor] = { ...serialized[cursor], ...wireReplay };
25
40
  }
26
41
  cursor += 1;
27
42
  }
28
43
  return serialized;
29
44
  }
45
+ /**
46
+ * Throw-free pre-flight check: which assistant tool-call turns in `context`
47
+ * lack the non-empty reasoning replay a reasoning-requiring provider needs.
48
+ */
49
+ assessReasoning(context) {
50
+ return assessReasoningReplay(context.turns, {
51
+ replayForAssistant: message => this.replayFields.get(assistantReplayKey(message)),
52
+ });
53
+ }
30
54
  normalizeToolCalls(toolCalls = []) {
31
55
  return toolCalls
32
56
  .filter((tc) => tc.type === "function")
@@ -40,3 +64,13 @@ export class OpenAIChatAdapter {
40
64
  return this.replayFields.get(assistantReplayKey(message));
41
65
  }
42
66
  }
67
+ function openAIChatWireReplayFields(replay) {
68
+ if (!replay)
69
+ return undefined;
70
+ const fields = {};
71
+ if (typeof replay.reasoning_content === "string")
72
+ fields.reasoning_content = replay.reasoning_content;
73
+ if (replay.reasoning_details !== undefined)
74
+ fields.reasoning_details = replay.reasoning_details;
75
+ return Object.keys(fields).length ? fields : undefined;
76
+ }
@@ -1,7 +1,8 @@
1
1
  import OpenAI from "openai";
2
- import type { Message, ProviderReplay, RenderedContext, ToolSchema, StreamEvent, LLMProvider, RuntimePolicy } from "../types.js";
2
+ import type { Message, ProviderDescriptor, ProviderReplay, RenderedContext, ToolSchema, StreamEvent, LLMProvider, RuntimePolicy } from "../types.js";
3
3
  import { CircuitBreaker } from "./base.js";
4
4
  import { OpenAIChatAdapter } from "./openai-chat.js";
5
+ import type { ReplayabilityAssessment } from "./replay-validator.js";
5
6
  export declare class OpenAIChatProvider implements LLMProvider {
6
7
  protected readonly model: string;
7
8
  protected client: OpenAI;
@@ -14,6 +15,18 @@ export declare class OpenAIChatProvider implements LLMProvider {
14
15
  baseDelay: number;
15
16
  }, baseURL?: string);
16
17
  runtimePolicy(): RuntimePolicy;
18
+ descriptor(): ProviderDescriptor;
19
+ protected requireNonEmptyReasoningReplayForToolTurns(_extensions?: Record<string, unknown>): boolean;
20
+ protected degradeMissingReasoningReplay(extensions?: Record<string, unknown>): boolean;
21
+ protected buildChatMessages(context: RenderedContext, extensions?: Record<string, unknown>): OpenAI.Chat.Completions.ChatCompletionMessageParam[];
22
+ /**
23
+ * Pre-flight query: would this history validate against this provider with the
24
+ * given extensions, without sending the request? Lets an embedder route around
25
+ * a reasoning-replay failure (keep thinking on, disable it, or skip this
26
+ * candidate) before issuing the request. `ok: true` when this provider does
27
+ * not require reasoning replay for the current extensions.
28
+ */
29
+ assessReplayability(context: RenderedContext, extensions?: Record<string, unknown>): ReplayabilityAssessment;
17
30
  peekProviderReplay(message: Pick<Message, "content" | "toolCalls">): ProviderReplay | undefined;
18
31
  seedProviderReplay(message: Pick<Message, "content" | "toolCalls">, replay: ProviderReplay): void;
19
32
  complete(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>): Promise<Message>;
@@ -42,21 +42,62 @@ export class OpenAIChatProvider {
42
42
  runtimePolicy() {
43
43
  return OPENAI_POLICIES[this.model] ?? {};
44
44
  }
45
+ descriptor() {
46
+ return {
47
+ provider: "openai",
48
+ protocol: "openai-chat",
49
+ model: this.model,
50
+ reasoning: {
51
+ supported: true,
52
+ preserveAcrossToolTurns: false,
53
+ },
54
+ toolCalls: {
55
+ supported: true,
56
+ requiresStrictPairing: true,
57
+ },
58
+ };
59
+ }
60
+ requireNonEmptyReasoningReplayForToolTurns(_extensions) {
61
+ return false;
62
+ }
63
+ degradeMissingReasoningReplay(extensions) {
64
+ return extensions?.degradeMissingReasoningReplay === true;
65
+ }
66
+ buildChatMessages(context, extensions) {
67
+ return this.chat.buildMessages(context, {
68
+ descriptor: this.descriptor(),
69
+ requireNonEmptyReasoningForToolCalls: this.requireNonEmptyReasoningReplayForToolTurns(extensions),
70
+ degradeMissingReasoning: this.degradeMissingReasoningReplay(extensions),
71
+ });
72
+ }
73
+ /**
74
+ * Pre-flight query: would this history validate against this provider with the
75
+ * given extensions, without sending the request? Lets an embedder route around
76
+ * a reasoning-replay failure (keep thinking on, disable it, or skip this
77
+ * candidate) before issuing the request. `ok: true` when this provider does
78
+ * not require reasoning replay for the current extensions.
79
+ */
80
+ assessReplayability(context, extensions) {
81
+ if (!this.requireNonEmptyReasoningReplayForToolTurns(extensions)) {
82
+ return { ok: true, offendingCallIds: [] };
83
+ }
84
+ return this.chat.assessReasoning(context);
85
+ }
45
86
  peekProviderReplay(message) {
46
87
  const fields = this.chat.peekReplayFields(message);
47
- if (!fields || !("reasoning_content" in fields))
88
+ if (!fields || !("reasoning_content" in fields || "reasoning_details" in fields))
48
89
  return undefined;
49
- return { reasoning_content: String(fields.reasoning_content ?? "") };
90
+ return fields;
50
91
  }
51
92
  seedProviderReplay(message, replay) {
52
- if (replay.reasoning_content !== undefined) {
53
- this.chat.rememberReplayFields(message, { reasoning_content: replay.reasoning_content });
93
+ if (replay.reasoning_content !== undefined || replay.reasoning_details !== undefined) {
94
+ this.chat.rememberReplayFields(message, replay);
54
95
  }
55
96
  }
56
97
  async complete(context, tools, extensions) {
57
98
  if (this.circuit.isOpen())
58
99
  throw new Error("Circuit breaker open");
59
- const msgs = this.chat.buildMessages(context);
100
+ const msgs = this.buildChatMessages(context, extensions);
60
101
  let lastErr;
61
102
  for (let i = 0; i < this.maxRetries; i++) {
62
103
  try {
@@ -81,7 +122,7 @@ export class OpenAIChatProvider {
81
122
  throw lastErr;
82
123
  }
83
124
  async *stream(context, tools, extensions) {
84
- const msgs = this.chat.buildMessages(context);
125
+ const msgs = this.buildChatMessages(context, extensions);
85
126
  const toolCallBufs = {};
86
127
  const emittedToolCallIndexes = new Set();
87
128
  const extractor = new ThinkingTagStreamExtractor();
@@ -190,7 +231,7 @@ export class OpenAIChatProvider {
190
231
  yield { type: "usage", totalTokens, inputTokens, outputTokens };
191
232
  }
192
233
  requestExtensions(extensions) {
193
- return omitExtensionKeys(extensions, ["model", "messages", "tools", "stream", "stream_options"]);
234
+ return omitExtensionKeys(extensions, ["model", "messages", "tools", "stream", "stream_options", "__deepstrikeThinkingEnabled"]);
194
235
  }
195
236
  }
196
237
  export { OpenAIChatProvider as OpenAIProvider };
@@ -59,6 +59,12 @@ export declare const endpointProfiles: {
59
59
  readonly protocol: "anthropic-messages";
60
60
  readonly baseURL: "https://api.minimaxi.com/anthropic";
61
61
  };
62
+ readonly "minimax.openai": {
63
+ readonly id: "minimax.openai";
64
+ readonly providerId: "minimax";
65
+ readonly protocol: "openai-chat";
66
+ readonly baseURL: "https://api.minimaxi.com/v1";
67
+ };
62
68
  readonly "deepseek.openai": {
63
69
  readonly id: "deepseek.openai";
64
70
  readonly providerId: "deepseek";
@@ -29,6 +29,12 @@ export const endpointProfiles = {
29
29
  protocol: "anthropic-messages",
30
30
  baseURL: "https://api.minimaxi.com/anthropic",
31
31
  },
32
+ "minimax.openai": {
33
+ id: "minimax.openai",
34
+ providerId: "minimax",
35
+ protocol: "openai-chat",
36
+ baseURL: "https://api.minimaxi.com/v1",
37
+ },
32
38
  "deepseek.openai": {
33
39
  id: "deepseek.openai",
34
40
  providerId: "deepseek",
@@ -1,5 +1,5 @@
1
1
  import OpenAI from "openai";
2
- import type { LLMProvider, Message, RenderedContext, StreamEvent, ToolSchema, RuntimePolicy, ProviderReplay } from "../types.js";
2
+ import type { LLMProvider, Message, ProviderDescriptor, RenderedContext, StreamEvent, ToolSchema, RuntimePolicy, ProviderReplay } from "../types.js";
3
3
  import { CircuitBreaker } from "./base.js";
4
4
  import { OpenAIChatAdapter } from "./openai-chat.js";
5
5
  export declare class QwenProvider implements LLMProvider {
@@ -14,6 +14,8 @@ export declare class QwenProvider implements LLMProvider {
14
14
  baseDelay: number;
15
15
  }, baseURL?: string);
16
16
  runtimePolicy(): RuntimePolicy;
17
+ descriptor(): ProviderDescriptor;
18
+ private buildChatMessages;
17
19
  peekProviderReplay(message: Pick<Message, "content" | "toolCalls">): ProviderReplay | undefined;
18
20
  seedProviderReplay(message: Pick<Message, "content" | "toolCalls">, replay: ProviderReplay): void;
19
21
  complete(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>): Promise<Message>;
@@ -36,6 +36,24 @@ export class QwenProvider {
36
36
  runtimePolicy() {
37
37
  return QWEN_POLICIES[this.model] ?? {};
38
38
  }
39
+ descriptor() {
40
+ return {
41
+ provider: "qwen",
42
+ protocol: "openai-chat",
43
+ model: this.model,
44
+ reasoning: {
45
+ supported: true,
46
+ preserveAcrossToolTurns: true,
47
+ },
48
+ toolCalls: {
49
+ supported: true,
50
+ requiresStrictPairing: true,
51
+ },
52
+ };
53
+ }
54
+ buildChatMessages(context) {
55
+ return this.chat.buildMessages(context, { descriptor: this.descriptor() });
56
+ }
39
57
  peekProviderReplay(message) {
40
58
  const fields = this.chat.peekReplayFields(message);
41
59
  if (!fields || !("reasoning_content" in fields))
@@ -50,7 +68,7 @@ export class QwenProvider {
50
68
  async complete(context, tools, extensions) {
51
69
  if (this.circuit.isOpen())
52
70
  throw new Error("Circuit breaker open");
53
- const msgs = this.chat.buildMessages(context);
71
+ const msgs = this.buildChatMessages(context);
54
72
  const extraBody = this.thinkingExtraBody(extensions);
55
73
  let lastErr;
56
74
  for (let i = 0; i < this.maxRetries; i++) {
@@ -77,7 +95,7 @@ export class QwenProvider {
77
95
  throw lastErr;
78
96
  }
79
97
  async *stream(context, tools, extensions) {
80
- const msgs = this.chat.buildMessages(context);
98
+ const msgs = this.buildChatMessages(context);
81
99
  const toolCallBufs = {};
82
100
  const emittedToolCallIndexes = new Set();
83
101
  let reasoningContent = "";
@@ -0,0 +1,32 @@
1
+ import type { Message, ProviderDescriptor, ProviderReplay, RenderedContext, ReplayabilityAssessment } from "../types.js";
2
+ export type { ReplayabilityAssessment };
3
+ export declare class ProviderReplayValidationError extends Error {
4
+ constructor(message: string);
5
+ }
6
+ /**
7
+ * Placeholder reasoning injected for an assistant tool-call turn that has no
8
+ * stored reasoning replay when the caller opted into graceful degradation
9
+ * (`degradeMissingReasoning`). It keeps the wire message well-formed for a
10
+ * thinking-on provider without fabricating substantive reasoning.
11
+ */
12
+ export declare const DEGRADED_REASONING_PLACEHOLDER = "[reasoning unavailable on replay]";
13
+ export interface OpenAIChatReplayValidationOptions {
14
+ descriptor?: ProviderDescriptor;
15
+ requireNonEmptyReasoningForToolCalls?: boolean;
16
+ /**
17
+ * When true, an assistant tool-call turn that lacks reasoning replay is
18
+ * degraded (serialized without/with a placeholder reasoning) instead of
19
+ * throwing. Lets a recovery/fallback request succeed in degraded form
20
+ * rather than fail outright.
21
+ */
22
+ degradeMissingReasoning?: boolean;
23
+ replayForAssistant?: (message: Pick<Message, "content" | "toolCalls">) => ProviderReplay | Record<string, unknown> | undefined;
24
+ }
25
+ export declare function validateOpenAIChatReplay(context: RenderedContext, options?: OpenAIChatReplayValidationOptions): void;
26
+ /**
27
+ * Pure, throw-free assessment: which assistant tool-call turns lack the
28
+ * non-empty reasoning replay a reasoning-requiring provider needs. Lets an
29
+ * embedder decide per-candidate whether to keep thinking on, disable it, or
30
+ * skip the candidate — before sending.
31
+ */
32
+ export declare function assessReasoningReplay(turns: Message[], options: Pick<OpenAIChatReplayValidationOptions, "replayForAssistant">): ReplayabilityAssessment;
@@ -0,0 +1,90 @@
1
+ export class ProviderReplayValidationError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "ProviderReplayValidationError";
5
+ }
6
+ }
7
+ /**
8
+ * Placeholder reasoning injected for an assistant tool-call turn that has no
9
+ * stored reasoning replay when the caller opted into graceful degradation
10
+ * (`degradeMissingReasoning`). It keeps the wire message well-formed for a
11
+ * thinking-on provider without fabricating substantive reasoning.
12
+ */
13
+ export const DEGRADED_REASONING_PLACEHOLDER = "[reasoning unavailable on replay]";
14
+ export function validateOpenAIChatReplay(context, options = {}) {
15
+ validateStrictToolResultPairing(context.turns);
16
+ if (options.requireNonEmptyReasoningForToolCalls && !options.degradeMissingReasoning) {
17
+ const assessment = assessReasoningReplay(context.turns, options);
18
+ if (!assessment.ok) {
19
+ throw reasoningReplayError(assessment.offendingCallIds, options.descriptor);
20
+ }
21
+ }
22
+ }
23
+ /**
24
+ * Pure, throw-free assessment: which assistant tool-call turns lack the
25
+ * non-empty reasoning replay a reasoning-requiring provider needs. Lets an
26
+ * embedder decide per-candidate whether to keep thinking on, disable it, or
27
+ * skip the candidate — before sending.
28
+ */
29
+ export function assessReasoningReplay(turns, options) {
30
+ const offendingCallIds = [];
31
+ for (const message of turns) {
32
+ if (message.role !== "assistant" || !message.toolCalls?.length)
33
+ continue;
34
+ const replay = options.replayForAssistant?.(message);
35
+ const reasoning = typeof replay?.reasoning_content === "string" ? replay.reasoning_content.trim() : "";
36
+ if (!reasoning) {
37
+ for (const tc of message.toolCalls)
38
+ offendingCallIds.push(tc.id);
39
+ }
40
+ }
41
+ return { ok: offendingCallIds.length === 0, offendingCallIds };
42
+ }
43
+ function reasoningReplayError(callIds, descriptor) {
44
+ const provider = descriptor ? `${descriptor.provider}/${descriptor.model}` : "provider";
45
+ return new ProviderReplayValidationError(`${provider} replay requires non-empty reasoning_content for assistant tool call turn ${callIds.join(", ")}. ` +
46
+ "Disable thinking, rebuild this history with provider replay, switch to a provider that can replay this turn, " +
47
+ "or pass extensions.degradeMissingReasoningReplay to send a degraded turn.");
48
+ }
49
+ function toolResultParts(message) {
50
+ return (message.contentParts ?? [])
51
+ .filter((part) => part.type === "tool_result");
52
+ }
53
+ function validateStrictToolResultPairing(turns) {
54
+ let pendingIds;
55
+ let completedIds = new Set();
56
+ const assertAllCompleted = () => {
57
+ if (!pendingIds)
58
+ return;
59
+ const missing = [...pendingIds].filter(id => !completedIds.has(id));
60
+ if (missing.length) {
61
+ throw new ProviderReplayValidationError(`OpenAI-compatible replay has assistant tool_calls with no tool result for ${missing.join(", ")}: ` +
62
+ "every tool_call must be answered by a tool message before the next assistant or user turn.");
63
+ }
64
+ };
65
+ for (const message of turns) {
66
+ if (message.role === "assistant") {
67
+ assertAllCompleted();
68
+ const toolCalls = message.toolCalls ?? [];
69
+ pendingIds = toolCalls.length ? new Set(toolCalls.map(tc => tc.id)) : undefined;
70
+ completedIds = new Set();
71
+ continue;
72
+ }
73
+ if (message.role !== "tool") {
74
+ assertAllCompleted();
75
+ pendingIds = undefined;
76
+ completedIds = new Set();
77
+ continue;
78
+ }
79
+ for (const part of toolResultParts(message)) {
80
+ if (!pendingIds?.has(part.callId)) {
81
+ throw new ProviderReplayValidationError(`OpenAI-compatible replay has orphan tool result ${part.callId}: no preceding assistant tool_call with the same id.`);
82
+ }
83
+ if (completedIds.has(part.callId)) {
84
+ throw new ProviderReplayValidationError(`OpenAI-compatible replay has duplicate tool result ${part.callId}.`);
85
+ }
86
+ completedIds.add(part.callId);
87
+ }
88
+ }
89
+ assertAllCompleted();
90
+ }