@falai/agent 2.4.3 → 2.6.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.
- package/dist/cjs/core/FlowRouter.d.ts.map +1 -1
- package/dist/cjs/core/FlowRouter.js +0 -11
- package/dist/cjs/core/FlowRouter.js.map +1 -1
- package/dist/cjs/core/ResponseModal.d.ts +51 -2
- package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
- package/dist/cjs/core/ResponseModal.js +272 -269
- package/dist/cjs/core/ResponseModal.js.map +1 -1
- package/dist/cjs/core/ToolLoopExecutor.d.ts +11 -0
- package/dist/cjs/core/ToolLoopExecutor.d.ts.map +1 -1
- package/dist/cjs/core/ToolLoopExecutor.js +75 -51
- package/dist/cjs/core/ToolLoopExecutor.js.map +1 -1
- package/dist/cjs/index.d.ts +3 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +4 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.js +18 -35
- package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
- package/dist/cjs/providers/DeepSeekProvider.d.ts +1 -12
- package/dist/cjs/providers/DeepSeekProvider.d.ts.map +1 -1
- package/dist/cjs/providers/DeepSeekProvider.js +3 -20
- package/dist/cjs/providers/DeepSeekProvider.js.map +1 -1
- package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/cjs/providers/GeminiProvider.js +22 -37
- package/dist/cjs/providers/GeminiProvider.js.map +1 -1
- package/dist/cjs/providers/GenericOpenAICompatibleProvider.d.ts +69 -0
- package/dist/cjs/providers/GenericOpenAICompatibleProvider.d.ts.map +1 -0
- package/dist/cjs/providers/GenericOpenAICompatibleProvider.js +93 -0
- package/dist/cjs/providers/GenericOpenAICompatibleProvider.js.map +1 -0
- package/dist/cjs/providers/OpenAICompatibleProvider.d.ts +23 -5
- package/dist/cjs/providers/OpenAICompatibleProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenAICompatibleProvider.js +46 -44
- package/dist/cjs/providers/OpenAICompatibleProvider.js.map +1 -1
- package/dist/cjs/providers/index.d.ts +3 -1
- package/dist/cjs/providers/index.d.ts.map +1 -1
- package/dist/cjs/providers/index.js +3 -1
- package/dist/cjs/providers/index.js.map +1 -1
- package/dist/cjs/utils/completion.d.ts +20 -0
- package/dist/cjs/utils/completion.d.ts.map +1 -0
- package/dist/cjs/utils/completion.js +33 -0
- package/dist/cjs/utils/completion.js.map +1 -0
- package/dist/cjs/utils/index.d.ts +3 -2
- package/dist/cjs/utils/index.d.ts.map +1 -1
- package/dist/cjs/utils/index.js +7 -1
- package/dist/cjs/utils/index.js.map +1 -1
- package/dist/cjs/utils/retry.d.ts +43 -7
- package/dist/cjs/utils/retry.d.ts.map +1 -1
- package/dist/cjs/utils/retry.js +100 -12
- package/dist/cjs/utils/retry.js.map +1 -1
- package/dist/cjs/utils/streamingMessage.d.ts +48 -0
- package/dist/cjs/utils/streamingMessage.d.ts.map +1 -0
- package/dist/cjs/utils/streamingMessage.js +210 -0
- package/dist/cjs/utils/streamingMessage.js.map +1 -0
- package/dist/core/FlowRouter.d.ts.map +1 -1
- package/dist/core/FlowRouter.js +0 -11
- package/dist/core/FlowRouter.js.map +1 -1
- package/dist/core/ResponseModal.d.ts +51 -2
- package/dist/core/ResponseModal.d.ts.map +1 -1
- package/dist/core/ResponseModal.js +272 -269
- package/dist/core/ResponseModal.js.map +1 -1
- package/dist/core/ToolLoopExecutor.d.ts +11 -0
- package/dist/core/ToolLoopExecutor.d.ts.map +1 -1
- package/dist/core/ToolLoopExecutor.js +76 -52
- package/dist/core/ToolLoopExecutor.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/providers/AnthropicProvider.js +19 -36
- package/dist/providers/AnthropicProvider.js.map +1 -1
- package/dist/providers/DeepSeekProvider.d.ts +1 -12
- package/dist/providers/DeepSeekProvider.d.ts.map +1 -1
- package/dist/providers/DeepSeekProvider.js +3 -20
- package/dist/providers/DeepSeekProvider.js.map +1 -1
- package/dist/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/providers/GeminiProvider.js +23 -38
- package/dist/providers/GeminiProvider.js.map +1 -1
- package/dist/providers/GenericOpenAICompatibleProvider.d.ts +69 -0
- package/dist/providers/GenericOpenAICompatibleProvider.d.ts.map +1 -0
- package/dist/providers/GenericOpenAICompatibleProvider.js +87 -0
- package/dist/providers/GenericOpenAICompatibleProvider.js.map +1 -0
- package/dist/providers/OpenAICompatibleProvider.d.ts +23 -5
- package/dist/providers/OpenAICompatibleProvider.d.ts.map +1 -1
- package/dist/providers/OpenAICompatibleProvider.js +47 -45
- package/dist/providers/OpenAICompatibleProvider.js.map +1 -1
- package/dist/providers/index.d.ts +3 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/utils/completion.d.ts +20 -0
- package/dist/utils/completion.d.ts.map +1 -0
- package/dist/utils/completion.js +29 -0
- package/dist/utils/completion.js.map +1 -0
- package/dist/utils/index.d.ts +3 -2
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +3 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/retry.d.ts +43 -7
- package/dist/utils/retry.d.ts.map +1 -1
- package/dist/utils/retry.js +98 -12
- package/dist/utils/retry.js.map +1 -1
- package/dist/utils/streamingMessage.d.ts +48 -0
- package/dist/utils/streamingMessage.d.ts.map +1 -0
- package/dist/utils/streamingMessage.js +205 -0
- package/dist/utils/streamingMessage.js.map +1 -0
- package/package.json +1 -1
- package/src/core/FlowRouter.ts +0 -14
- package/src/core/ResponseModal.ts +332 -299
- package/src/core/ToolLoopExecutor.ts +108 -52
- package/src/index.ts +3 -0
- package/src/providers/AnthropicProvider.ts +22 -43
- package/src/providers/DeepSeekProvider.ts +3 -35
- package/src/providers/GeminiProvider.ts +27 -46
- package/src/providers/GenericOpenAICompatibleProvider.ts +138 -0
- package/src/providers/OpenAICompatibleProvider.ts +87 -67
- package/src/providers/index.ts +4 -0
- package/src/utils/completion.ts +38 -0
- package/src/utils/index.ts +5 -2
- package/src/utils/retry.ts +136 -14
- package/src/utils/streamingMessage.ts +220 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic OpenAI-compatible provider + factory.
|
|
3
|
+
*
|
|
4
|
+
* Most "OpenAI-compatible" endpoints (Azure OpenAI, Groq, Together, Fireworks,
|
|
5
|
+
* vLLM, LM Studio, Ollama, a self-hosted gateway…) differ from OpenAI only in
|
|
6
|
+
* base URL, headers, and how structured output is requested. `createOpenAICompatibleProvider`
|
|
7
|
+
* builds a working provider from that config alone — no subclass required — so
|
|
8
|
+
* adding one ("fake" provider, same wire protocol, different base URL) is a few lines:
|
|
9
|
+
*
|
|
10
|
+
* const ollama = createOpenAICompatibleProvider({
|
|
11
|
+
* name: "ollama",
|
|
12
|
+
* baseURL: "http://localhost:11434/v1",
|
|
13
|
+
* apiKey: "ollama", // local servers ignore it; pass any non-empty string
|
|
14
|
+
* model: "llama3.3",
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* const azure = createOpenAICompatibleProvider({
|
|
18
|
+
* name: "azure",
|
|
19
|
+
* baseURL: `https://${resource}.openai.azure.com/openai/deployments/${deployment}`,
|
|
20
|
+
* apiKey: process.env.AZURE_OPENAI_KEY!,
|
|
21
|
+
* model: deployment,
|
|
22
|
+
* defaultHeaders: { "api-key": process.env.AZURE_OPENAI_KEY! },
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* For OpenAI/OpenRouter (native `responses.parse`) and Anthropic/Gemini, prefer
|
|
26
|
+
* their dedicated provider classes.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import OpenAI from "openai";
|
|
30
|
+
|
|
31
|
+
import type { ProviderCapabilities } from "../types/ai";
|
|
32
|
+
import {
|
|
33
|
+
OpenAICompatibleProvider,
|
|
34
|
+
type OpenAICompatibleRequestConfig,
|
|
35
|
+
type StructuredOutputMode,
|
|
36
|
+
} from "./OpenAICompatibleProvider";
|
|
37
|
+
|
|
38
|
+
export interface OpenAICompatibleOptions {
|
|
39
|
+
/** Provider identifier, e.g. "azure", "ollama", "groq". */
|
|
40
|
+
name: string;
|
|
41
|
+
/** Base URL of the OpenAI-compatible endpoint. */
|
|
42
|
+
baseURL: string;
|
|
43
|
+
/** API key. Local servers often ignore it — pass any non-empty string. */
|
|
44
|
+
apiKey: string;
|
|
45
|
+
/** Primary model / deployment name. */
|
|
46
|
+
model: string;
|
|
47
|
+
/** Backup models to try if the primary fails. */
|
|
48
|
+
backupModels?: string[];
|
|
49
|
+
/** Human-readable name for error messages. Defaults to `name`. */
|
|
50
|
+
displayName?: string;
|
|
51
|
+
/** Uppercase tag for log lines. Defaults to `name.toUpperCase()`. */
|
|
52
|
+
logLabel?: string;
|
|
53
|
+
/** Capability overrides, merged over the defaults (all true except caching). */
|
|
54
|
+
capabilities?: Partial<ProviderCapabilities>;
|
|
55
|
+
/** Extra request headers (e.g. Azure's `api-key`, a gateway's auth header). */
|
|
56
|
+
defaultHeaders?: Record<string, string>;
|
|
57
|
+
/**
|
|
58
|
+
* How structured output is requested. Defaults to `"json_schema"` — the
|
|
59
|
+
* broadest enforced mode for arbitrary compatible endpoints (`responses.parse`
|
|
60
|
+
* is OpenAI-only). See {@link StructuredOutputMode}.
|
|
61
|
+
*/
|
|
62
|
+
structuredOutput?: StructuredOutputMode;
|
|
63
|
+
/** Default request parameters merged into every call. */
|
|
64
|
+
config?: OpenAICompatibleRequestConfig;
|
|
65
|
+
/** Per-call timeout (ms) and retry count. */
|
|
66
|
+
retryConfig?: { timeout?: number; retries?: number };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Sensible defaults for a modern OpenAI-compatible endpoint. */
|
|
70
|
+
const DEFAULT_CAPABILITIES: ProviderCapabilities = {
|
|
71
|
+
supportsTools: true,
|
|
72
|
+
supportsNativeJsonSchema: true,
|
|
73
|
+
supportsStreaming: true,
|
|
74
|
+
supportsStreamingToolCalls: true,
|
|
75
|
+
supportsPromptCaching: false,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Concrete OpenAI-compatible provider configured entirely from
|
|
80
|
+
* {@link OpenAICompatibleOptions}. Kept private — construct via
|
|
81
|
+
* {@link createOpenAICompatibleProvider}; subclass {@link OpenAICompatibleProvider}
|
|
82
|
+
* directly if you need behavior beyond these knobs (e.g. provider-specific
|
|
83
|
+
* error classification or streaming hooks).
|
|
84
|
+
*/
|
|
85
|
+
class GenericOpenAICompatibleProvider extends OpenAICompatibleProvider {
|
|
86
|
+
public readonly name: string;
|
|
87
|
+
public readonly capabilities: ProviderCapabilities;
|
|
88
|
+
protected readonly logLabel: string;
|
|
89
|
+
protected readonly displayName: string;
|
|
90
|
+
|
|
91
|
+
constructor(options: OpenAICompatibleOptions) {
|
|
92
|
+
if (!options.name) {
|
|
93
|
+
throw new Error("An OpenAI-compatible provider needs a `name`.");
|
|
94
|
+
}
|
|
95
|
+
if (!options.baseURL) {
|
|
96
|
+
throw new Error(`[${options.name}] A \`baseURL\` is required.`);
|
|
97
|
+
}
|
|
98
|
+
if (!options.apiKey) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`[${options.name}] An \`apiKey\` is required — use any non-empty string for servers that ignore it.`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
if (!options.model) {
|
|
104
|
+
throw new Error(`[${options.name}] A \`model\` is required.`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
super({
|
|
108
|
+
client: new OpenAI({
|
|
109
|
+
apiKey: options.apiKey,
|
|
110
|
+
baseURL: options.baseURL,
|
|
111
|
+
...(options.defaultHeaders ? { defaultHeaders: options.defaultHeaders } : {}),
|
|
112
|
+
}),
|
|
113
|
+
model: options.model,
|
|
114
|
+
backupModels: options.backupModels,
|
|
115
|
+
config: options.config,
|
|
116
|
+
retryConfig: options.retryConfig,
|
|
117
|
+
// Arbitrary compatible endpoints rarely have responses.parse; default to
|
|
118
|
+
// the broadly-supported chat json_schema strategy.
|
|
119
|
+
structuredOutput: options.structuredOutput ?? "json_schema",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
this.name = options.name;
|
|
123
|
+
this.displayName = options.displayName ?? options.name;
|
|
124
|
+
this.logLabel = options.logLabel ?? options.name.toUpperCase();
|
|
125
|
+
this.capabilities = { ...DEFAULT_CAPABILITIES, ...options.capabilities };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Build an `AiProvider` for any OpenAI-compatible endpoint from config alone.
|
|
131
|
+
* See {@link OpenAICompatibleOptions} for the knobs and the file header for
|
|
132
|
+
* Azure/Ollama examples.
|
|
133
|
+
*/
|
|
134
|
+
export function createOpenAICompatibleProvider(
|
|
135
|
+
options: OpenAICompatibleOptions
|
|
136
|
+
): OpenAICompatibleProvider {
|
|
137
|
+
return new GenericOpenAICompatibleProvider(options);
|
|
138
|
+
}
|
|
@@ -27,7 +27,7 @@ import type {
|
|
|
27
27
|
} from "../types";
|
|
28
28
|
import type { ProviderCapabilities } from "../types/ai";
|
|
29
29
|
import type { HistoryItem } from "../types/history";
|
|
30
|
-
import { withTimeoutAndRetry, withStreamRetry, logger } from "../utils";
|
|
30
|
+
import { withTimeoutAndRetry, withStreamRetry, resolveRetryConfig, logger, assertUsableCompletion, combineAbortSignals } from "../utils";
|
|
31
31
|
import {
|
|
32
32
|
classifyProviderError,
|
|
33
33
|
getErrorMessage,
|
|
@@ -36,11 +36,6 @@ import {
|
|
|
36
36
|
type ErrorClassificationOptions,
|
|
37
37
|
} from "./errorClassification";
|
|
38
38
|
|
|
39
|
-
const DEFAULT_RETRY_CONFIG = {
|
|
40
|
-
timeout: 60000,
|
|
41
|
-
retries: 3,
|
|
42
|
-
};
|
|
43
|
-
|
|
44
39
|
/**
|
|
45
40
|
* Default request parameters shared by OpenAI-compatible providers
|
|
46
41
|
*/
|
|
@@ -48,6 +43,17 @@ export type OpenAICompatibleRequestConfig = Partial<
|
|
|
48
43
|
Omit<ChatCompletionCreateParamsNonStreaming, "model" | "messages">
|
|
49
44
|
>;
|
|
50
45
|
|
|
46
|
+
/**
|
|
47
|
+
* How structured (JSON-schema) output is requested from the endpoint:
|
|
48
|
+
* - `"responses_parse"` (default) — OpenAI's native `responses.parse` API
|
|
49
|
+
* (OpenAI, OpenRouter). Most compatible endpoints do not implement it.
|
|
50
|
+
* - `"json_schema"` — chat completions with a `json_schema` response_format.
|
|
51
|
+
* The broadest enforced mode (DeepSeek, Groq, Together, Fireworks, vLLM, Azure…).
|
|
52
|
+
* - `"json_object"` — chat completions with a `json_object` response_format,
|
|
53
|
+
* for servers that lack json_schema enforcement (parsed at the end).
|
|
54
|
+
*/
|
|
55
|
+
export type StructuredOutputMode = "responses_parse" | "json_schema" | "json_object";
|
|
56
|
+
|
|
51
57
|
/**
|
|
52
58
|
* Initialization values supplied by subclasses
|
|
53
59
|
*/
|
|
@@ -65,6 +71,11 @@ export interface OpenAICompatibleProviderInit {
|
|
|
65
71
|
timeout?: number;
|
|
66
72
|
retries?: number;
|
|
67
73
|
};
|
|
74
|
+
/**
|
|
75
|
+
* How structured output is requested (default `"responses_parse"`). Lifts the
|
|
76
|
+
* strategy that used to be a per-subclass override into shared config.
|
|
77
|
+
*/
|
|
78
|
+
structuredOutput?: StructuredOutputMode;
|
|
68
79
|
}
|
|
69
80
|
|
|
70
81
|
/**
|
|
@@ -85,6 +96,8 @@ export abstract class OpenAICompatibleProvider implements AiProvider {
|
|
|
85
96
|
protected readonly backupModels: string[];
|
|
86
97
|
protected readonly config?: OpenAICompatibleRequestConfig;
|
|
87
98
|
protected readonly retryConfig: { timeout: number; retries: number };
|
|
99
|
+
/** Structured-output strategy; see {@link StructuredOutputMode}. */
|
|
100
|
+
protected readonly structuredOutput: StructuredOutputMode;
|
|
88
101
|
|
|
89
102
|
/**
|
|
90
103
|
* Provider-specific error classification signals.
|
|
@@ -99,14 +112,8 @@ export abstract class OpenAICompatibleProvider implements AiProvider {
|
|
|
99
112
|
this.primaryModel = init.model;
|
|
100
113
|
this.backupModels = init.backupModels ?? [];
|
|
101
114
|
this.config = init.config;
|
|
102
|
-
this.retryConfig =
|
|
103
|
-
|
|
104
|
-
// immediately), so fall back to the default.
|
|
105
|
-
timeout: init.retryConfig?.timeout || DEFAULT_RETRY_CONFIG.timeout,
|
|
106
|
-
// `??` so an explicit `retries: 0` (disable retries) is honored instead of
|
|
107
|
-
// being clobbered to the default by a falsy-zero check.
|
|
108
|
-
retries: init.retryConfig?.retries ?? DEFAULT_RETRY_CONFIG.retries,
|
|
109
|
-
};
|
|
115
|
+
this.retryConfig = resolveRetryConfig(init.retryConfig);
|
|
116
|
+
this.structuredOutput = init.structuredOutput ?? "responses_parse";
|
|
110
117
|
}
|
|
111
118
|
|
|
112
119
|
// ---------------------------------------------------------------------
|
|
@@ -146,22 +153,31 @@ export abstract class OpenAICompatibleProvider implements AiProvider {
|
|
|
146
153
|
input: GenerateMessageInput<unknown>,
|
|
147
154
|
jsonSchema: StructuredSchema
|
|
148
155
|
): Promise<GenerateMessageOutput> {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
156
|
+
// Non-native modes go through chat completions, where
|
|
157
|
+
// `structuredResponseFormat` selects the response_format.
|
|
158
|
+
if (this.structuredOutput !== "responses_parse") {
|
|
159
|
+
return this.executeChatCompletion(model, input);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const response = await this.client.responses.parse(
|
|
163
|
+
{
|
|
164
|
+
model,
|
|
165
|
+
instructions: input.prompt,
|
|
166
|
+
input: "",
|
|
167
|
+
reasoning: {
|
|
168
|
+
effort: input.parameters?.reasoning?.effort || "low",
|
|
169
|
+
},
|
|
170
|
+
text: {
|
|
171
|
+
format: {
|
|
172
|
+
type: "json_schema",
|
|
173
|
+
name: input.parameters?.schemaName || "structured_output",
|
|
174
|
+
// Adapt common schema format to the provider's format
|
|
175
|
+
schema: this.adaptSchema(jsonSchema),
|
|
176
|
+
},
|
|
162
177
|
},
|
|
163
178
|
},
|
|
164
|
-
|
|
179
|
+
{ signal: input.signal }
|
|
180
|
+
);
|
|
165
181
|
|
|
166
182
|
if (!response.output_parsed) {
|
|
167
183
|
throw new Error(`No parsed output returned from ${this.displayName}`);
|
|
@@ -183,15 +199,25 @@ export abstract class OpenAICompatibleProvider implements AiProvider {
|
|
|
183
199
|
}
|
|
184
200
|
|
|
185
201
|
/**
|
|
186
|
-
* response_format applied to chat-completions requests when a JSON
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* completions
|
|
202
|
+
* response_format applied to chat-completions requests when a JSON schema is
|
|
203
|
+
* present, selected by {@link StructuredOutputMode}: `"json_schema"` for native
|
|
204
|
+
* enforcement, else a generic JSON object parsed at the end. (`"responses_parse"`
|
|
205
|
+
* never reaches chat completions for non-streaming, but streaming has no parse
|
|
206
|
+
* API, so it falls back to `json_object` here.)
|
|
190
207
|
*/
|
|
191
208
|
protected structuredResponseFormat(
|
|
192
|
-
|
|
193
|
-
|
|
209
|
+
jsonSchema: StructuredSchema,
|
|
210
|
+
schemaName: string | undefined
|
|
194
211
|
): ChatCompletionCreateParamsNonStreaming["response_format"] {
|
|
212
|
+
if (this.structuredOutput === "json_schema") {
|
|
213
|
+
return {
|
|
214
|
+
type: "json_schema" as const,
|
|
215
|
+
json_schema: {
|
|
216
|
+
name: schemaName || "structured_output",
|
|
217
|
+
schema: this.adaptSchema(jsonSchema),
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
195
221
|
return { type: "json_object" };
|
|
196
222
|
}
|
|
197
223
|
|
|
@@ -413,8 +439,8 @@ export abstract class OpenAICompatibleProvider implements AiProvider {
|
|
|
413
439
|
// Try primary model first
|
|
414
440
|
try {
|
|
415
441
|
yield* withStreamRetry(
|
|
416
|
-
() => this.generateStreamWithModel<TContext, TStructured>(this.primaryModel, input),
|
|
417
|
-
{ maxRetries: this.retryConfig.retries, operationName: `${this.logLabel} ${this.primaryModel} stream` }
|
|
442
|
+
(signal) => this.generateStreamWithModel<TContext, TStructured>(this.primaryModel, input, signal),
|
|
443
|
+
{ maxRetries: this.retryConfig.retries, firstChunkTimeoutMs: this.retryConfig.timeout, operationName: `${this.logLabel} ${this.primaryModel} stream` }
|
|
418
444
|
);
|
|
419
445
|
} catch (primaryError: unknown) {
|
|
420
446
|
const primaryErrMsg = getErrorMessage(primaryError);
|
|
@@ -440,8 +466,8 @@ export abstract class OpenAICompatibleProvider implements AiProvider {
|
|
|
440
466
|
|
|
441
467
|
try {
|
|
442
468
|
yield* withStreamRetry(
|
|
443
|
-
() => this.generateStreamWithModel<TContext, TStructured>(backupModel, input),
|
|
444
|
-
{ maxRetries: this.retryConfig.retries, operationName: `${this.logLabel} ${backupModel} stream` }
|
|
469
|
+
(signal) => this.generateStreamWithModel<TContext, TStructured>(backupModel, input, signal),
|
|
470
|
+
{ maxRetries: this.retryConfig.retries, firstChunkTimeoutMs: this.retryConfig.timeout, operationName: `${this.logLabel} ${backupModel} stream` }
|
|
445
471
|
);
|
|
446
472
|
logger.debug(`[${this.logLabel}] Backup model ${backupModel} succeeded`);
|
|
447
473
|
return;
|
|
@@ -483,16 +509,20 @@ export abstract class OpenAICompatibleProvider implements AiProvider {
|
|
|
483
509
|
model: string,
|
|
484
510
|
input: GenerateMessageInput<TContext>
|
|
485
511
|
): Promise<GenerateMessageOutput<TStructured>> {
|
|
486
|
-
const operation = async (): Promise<GenerateMessageOutput> => {
|
|
512
|
+
const operation = async (signal: AbortSignal): Promise<GenerateMessageOutput> => {
|
|
513
|
+
// Thread the per-attempt timeout signal through input.signal so whichever
|
|
514
|
+
// path runs — responses.parse (structured) or chat.completions — cancels
|
|
515
|
+
// its upstream call when the deadline fires.
|
|
516
|
+
const reqInput = { ...input, signal: combineAbortSignals(input.signal, signal) };
|
|
487
517
|
// Use structured output path if JSON schema is provided
|
|
488
|
-
if (
|
|
518
|
+
if (reqInput.parameters?.jsonSchema) {
|
|
489
519
|
return this.executeStructuredGenerate(
|
|
490
520
|
model,
|
|
491
|
-
|
|
492
|
-
|
|
521
|
+
reqInput,
|
|
522
|
+
reqInput.parameters.jsonSchema
|
|
493
523
|
);
|
|
494
524
|
}
|
|
495
|
-
return this.executeChatCompletion(model,
|
|
525
|
+
return this.executeChatCompletion(model, reqInput);
|
|
496
526
|
};
|
|
497
527
|
|
|
498
528
|
return withTimeoutAndRetry(
|
|
@@ -540,7 +570,9 @@ export abstract class OpenAICompatibleProvider implements AiProvider {
|
|
|
540
570
|
);
|
|
541
571
|
}
|
|
542
572
|
|
|
543
|
-
const response = await this.client.chat.completions.create(params
|
|
573
|
+
const response = await this.client.chat.completions.create(params, {
|
|
574
|
+
signal: input.signal,
|
|
575
|
+
});
|
|
544
576
|
|
|
545
577
|
const message = response.choices[0]?.message?.content || "";
|
|
546
578
|
|
|
@@ -584,16 +616,10 @@ export abstract class OpenAICompatibleProvider implements AiProvider {
|
|
|
584
616
|
} as AgentStructuredResponse;
|
|
585
617
|
}
|
|
586
618
|
|
|
587
|
-
// A parsed-but-blank
|
|
588
|
-
//
|
|
619
|
+
// A parsed-but-blank message with no tool calls is as empty as no text;
|
|
620
|
+
// the shared guard throws so withTimeoutAndRetry retries instead of
|
|
589
621
|
// returning {"message":""}.
|
|
590
|
-
|
|
591
|
-
toolCalls.length === 0 &&
|
|
592
|
-
typeof structured?.message === "string" &&
|
|
593
|
-
!structured.message.trim()
|
|
594
|
-
) {
|
|
595
|
-
throw new Error(`No response from ${this.displayName}`);
|
|
596
|
-
}
|
|
622
|
+
assertUsableCompletion(structured, message, toolCalls.length, this.displayName);
|
|
597
623
|
|
|
598
624
|
return {
|
|
599
625
|
message,
|
|
@@ -617,7 +643,8 @@ export abstract class OpenAICompatibleProvider implements AiProvider {
|
|
|
617
643
|
TStructured = AgentStructuredResponse
|
|
618
644
|
>(
|
|
619
645
|
model: string,
|
|
620
|
-
input: GenerateMessageInput<TContext
|
|
646
|
+
input: GenerateMessageInput<TContext>,
|
|
647
|
+
attemptSignal?: AbortSignal
|
|
621
648
|
): AsyncGenerator<GenerateMessageStreamChunk<TStructured>> {
|
|
622
649
|
// Build messages from history and append prompt as final user message
|
|
623
650
|
const historyMessages = this.buildChatMessages(input.history);
|
|
@@ -652,7 +679,9 @@ export abstract class OpenAICompatibleProvider implements AiProvider {
|
|
|
652
679
|
);
|
|
653
680
|
}
|
|
654
681
|
|
|
655
|
-
const stream = await this.client.chat.completions.create(params
|
|
682
|
+
const stream = await this.client.chat.completions.create(params, {
|
|
683
|
+
signal: combineAbortSignals(input.signal, attemptSignal),
|
|
684
|
+
});
|
|
656
685
|
|
|
657
686
|
let accumulated = "";
|
|
658
687
|
let currentModel = model;
|
|
@@ -735,18 +764,9 @@ export abstract class OpenAICompatibleProvider implements AiProvider {
|
|
|
735
764
|
} as TStructured;
|
|
736
765
|
}
|
|
737
766
|
|
|
738
|
-
// Empty-completion guard —
|
|
739
|
-
//
|
|
740
|
-
|
|
741
|
-
// so withStreamRetry/generateStreamWithBackup retry instead of silently
|
|
742
|
-
// emitting an empty message.
|
|
743
|
-
const structuredMessage = (structured as AgentStructuredResponse | undefined)?.message;
|
|
744
|
-
const messageText = (
|
|
745
|
-
typeof structuredMessage === "string" ? structuredMessage : accumulated
|
|
746
|
-
).trim();
|
|
747
|
-
if (!messageText && toolCalls.length === 0) {
|
|
748
|
-
throw new Error(`No response from ${this.displayName}`);
|
|
749
|
-
}
|
|
767
|
+
// Empty-completion guard — same definition as the non-streaming path, so the
|
|
768
|
+
// stream retries / falls back to backup instead of emitting an empty message.
|
|
769
|
+
assertUsableCompletion(structured, accumulated, toolCalls.length, this.displayName);
|
|
750
770
|
|
|
751
771
|
// Yield final chunk
|
|
752
772
|
yield {
|
package/src/providers/index.ts
CHANGED
|
@@ -22,8 +22,12 @@ export { OpenAICompatibleProvider } from "./OpenAICompatibleProvider";
|
|
|
22
22
|
export type {
|
|
23
23
|
OpenAICompatibleProviderInit,
|
|
24
24
|
OpenAICompatibleRequestConfig,
|
|
25
|
+
StructuredOutputMode,
|
|
25
26
|
} from "./OpenAICompatibleProvider";
|
|
26
27
|
|
|
28
|
+
export { createOpenAICompatibleProvider } from "./GenericOpenAICompatibleProvider";
|
|
29
|
+
export type { OpenAICompatibleOptions } from "./GenericOpenAICompatibleProvider";
|
|
30
|
+
|
|
27
31
|
export {
|
|
28
32
|
classifyProviderError,
|
|
29
33
|
isBackupEligible,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The user-facing text of a (possibly structured) model completion, trimmed.
|
|
3
|
+
*
|
|
4
|
+
* Under a JSON schema the real message lives in `structured.message`; otherwise
|
|
5
|
+
* it's the raw/accumulated text passed as `fallbackText`. A blank result with no
|
|
6
|
+
* tool calls means the model produced nothing usable — providers throw on that
|
|
7
|
+
* so the retry/backup path runs instead of emitting an empty message. Shared by
|
|
8
|
+
* the streaming and non-streaming empty-completion guards across all providers.
|
|
9
|
+
*/
|
|
10
|
+
export function effectiveMessageText(
|
|
11
|
+
structured: unknown,
|
|
12
|
+
fallbackText: string
|
|
13
|
+
): string {
|
|
14
|
+
const message =
|
|
15
|
+
typeof structured === "object" && structured !== null && "message" in structured
|
|
16
|
+
? structured.message
|
|
17
|
+
: undefined;
|
|
18
|
+
return (typeof message === "string" ? message : fallbackText).trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The single definition of "the model produced nothing usable": a blank
|
|
23
|
+
* effective message ({@link effectiveMessageText}) and no tool calls. When that
|
|
24
|
+
* holds, throw `No response from <provider>` so the caller's retry/backup path
|
|
25
|
+
* runs instead of surfacing an empty turn. Lifting this above the providers
|
|
26
|
+
* keeps the streaming and non-streaming guards — six call sites across three
|
|
27
|
+
* providers — from drifting on what counts as empty.
|
|
28
|
+
*/
|
|
29
|
+
export function assertUsableCompletion(
|
|
30
|
+
structured: unknown,
|
|
31
|
+
fallbackText: string,
|
|
32
|
+
toolCallCount: number,
|
|
33
|
+
providerLabel: string
|
|
34
|
+
): void {
|
|
35
|
+
if (toolCallCount === 0 && !effectiveMessageText(structured, fallbackText)) {
|
|
36
|
+
throw new Error(`No response from ${providerLabel}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -55,8 +55,11 @@ export {
|
|
|
55
55
|
export { LoggerLevel, logger } from "./logger";
|
|
56
56
|
|
|
57
57
|
// Retry utilities
|
|
58
|
-
export type { RetryOptions, StreamRetryOptions } from "./retry";
|
|
59
|
-
export { retry, withTimeoutAndRetry, withStreamRetry } from "./retry";
|
|
58
|
+
export type { RetryOptions, StreamRetryOptions, RetryConfig } from "./retry";
|
|
59
|
+
export { retry, withTimeoutAndRetry, withStreamRetry, resolveRetryConfig, combineAbortSignals } from "./retry";
|
|
60
|
+
|
|
61
|
+
// Completion helpers
|
|
62
|
+
export { effectiveMessageText, assertUsableCompletion } from "./completion";
|
|
60
63
|
|
|
61
64
|
// Condition utilities
|
|
62
65
|
export {
|