@falai/agent 2.4.2 → 2.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.
- package/dist/cjs/core/PersistenceManager.d.ts +13 -0
- package/dist/cjs/core/PersistenceManager.d.ts.map +1 -1
- package/dist/cjs/core/PersistenceManager.js +29 -6
- package/dist/cjs/core/PersistenceManager.js.map +1 -1
- package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
- package/dist/cjs/core/ResponseModal.js +15 -3
- package/dist/cjs/core/ResponseModal.js.map +1 -1
- package/dist/cjs/core/SessionManager.d.ts.map +1 -1
- package/dist/cjs/core/SessionManager.js +7 -0
- package/dist/cjs/core/SessionManager.js.map +1 -1
- package/dist/cjs/core/StreamingToolExecutor.d.ts.map +1 -1
- package/dist/cjs/core/StreamingToolExecutor.js +11 -0
- package/dist/cjs/core/StreamingToolExecutor.js.map +1 -1
- package/dist/cjs/core/ToolLoopExecutor.d.ts +27 -2
- package/dist/cjs/core/ToolLoopExecutor.d.ts.map +1 -1
- package/dist/cjs/core/ToolLoopExecutor.js +159 -74
- package/dist/cjs/core/ToolLoopExecutor.js.map +1 -1
- package/dist/cjs/core/ToolManager.d.ts.map +1 -1
- package/dist/cjs/core/ToolManager.js +8 -31
- package/dist/cjs/core/ToolManager.js.map +1 -1
- package/dist/cjs/core/toolGates.d.ts +24 -0
- package/dist/cjs/core/toolGates.d.ts.map +1 -0
- package/dist/cjs/core/toolGates.js +52 -0
- package/dist/cjs/core/toolGates.js.map +1 -0
- 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 -14
- 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 +19 -15
- 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 +47 -23
- 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 +8 -1
- package/dist/cjs/utils/index.js.map +1 -1
- package/dist/cjs/utils/retry.d.ts +58 -1
- package/dist/cjs/utils/retry.d.ts.map +1 -1
- package/dist/cjs/utils/retry.js +128 -3
- package/dist/cjs/utils/retry.js.map +1 -1
- package/dist/core/PersistenceManager.d.ts +13 -0
- package/dist/core/PersistenceManager.d.ts.map +1 -1
- package/dist/core/PersistenceManager.js +29 -6
- package/dist/core/PersistenceManager.js.map +1 -1
- package/dist/core/ResponseModal.d.ts.map +1 -1
- package/dist/core/ResponseModal.js +15 -3
- package/dist/core/ResponseModal.js.map +1 -1
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +7 -0
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/StreamingToolExecutor.d.ts.map +1 -1
- package/dist/core/StreamingToolExecutor.js +11 -0
- package/dist/core/StreamingToolExecutor.js.map +1 -1
- package/dist/core/ToolLoopExecutor.d.ts +27 -2
- package/dist/core/ToolLoopExecutor.d.ts.map +1 -1
- package/dist/core/ToolLoopExecutor.js +160 -75
- package/dist/core/ToolLoopExecutor.js.map +1 -1
- package/dist/core/ToolManager.d.ts.map +1 -1
- package/dist/core/ToolManager.js +8 -31
- package/dist/core/ToolManager.js.map +1 -1
- package/dist/core/toolGates.d.ts +24 -0
- package/dist/core/toolGates.d.ts.map +1 -0
- package/dist/core/toolGates.js +49 -0
- package/dist/core/toolGates.js.map +1 -0
- 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 -15
- 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 +20 -16
- 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 +48 -24
- 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 +58 -1
- package/dist/utils/retry.d.ts.map +1 -1
- package/dist/utils/retry.js +125 -3
- package/dist/utils/retry.js.map +1 -1
- package/package.json +1 -1
- package/src/core/PersistenceManager.ts +30 -6
- package/src/core/ResponseModal.ts +16 -3
- package/src/core/SessionManager.ts +7 -0
- package/src/core/StreamingToolExecutor.ts +12 -0
- package/src/core/ToolLoopExecutor.ts +210 -79
- package/src/core/ToolManager.ts +8 -32
- package/src/core/toolGates.ts +72 -0
- package/src/index.ts +3 -0
- package/src/providers/AnthropicProvider.ts +26 -20
- package/src/providers/DeepSeekProvider.ts +3 -35
- package/src/providers/GeminiProvider.ts +29 -17
- package/src/providers/GenericOpenAICompatibleProvider.ts +138 -0
- package/src/providers/OpenAICompatibleProvider.ts +92 -44
- 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 +181 -4
|
@@ -7,15 +7,9 @@
|
|
|
7
7
|
import OpenAI from "openai";
|
|
8
8
|
import type {
|
|
9
9
|
ChatCompletionChunk,
|
|
10
|
-
ChatCompletionCreateParamsNonStreaming,
|
|
11
10
|
ChatCompletionCreateParamsStreaming,
|
|
12
11
|
} from "openai/resources/chat/completions";
|
|
13
12
|
|
|
14
|
-
import type {
|
|
15
|
-
GenerateMessageInput,
|
|
16
|
-
GenerateMessageOutput,
|
|
17
|
-
StructuredSchema,
|
|
18
|
-
} from "../types";
|
|
19
13
|
import type { ProviderCapabilities } from "../types/ai";
|
|
20
14
|
import { logger } from "../utils";
|
|
21
15
|
import {
|
|
@@ -96,38 +90,12 @@ export class DeepSeekProvider extends OpenAICompatibleProvider {
|
|
|
96
90
|
backupModels,
|
|
97
91
|
config,
|
|
98
92
|
retryConfig,
|
|
93
|
+
// DeepSeek has no responses.parse API; structured output goes through
|
|
94
|
+
// chat completions with a native json_schema response_format.
|
|
95
|
+
structuredOutput: "json_schema",
|
|
99
96
|
});
|
|
100
97
|
}
|
|
101
98
|
|
|
102
|
-
/**
|
|
103
|
-
* DeepSeek has no responses.parse API; structured output goes through
|
|
104
|
-
* chat completions with a json_schema response_format.
|
|
105
|
-
*/
|
|
106
|
-
protected override async executeStructuredGenerate(
|
|
107
|
-
model: string,
|
|
108
|
-
input: GenerateMessageInput<unknown>,
|
|
109
|
-
_jsonSchema: StructuredSchema
|
|
110
|
-
): Promise<GenerateMessageOutput> {
|
|
111
|
-
return this.executeChatCompletion(model, input);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* DeepSeek supports native json_schema enforcement in chat completions
|
|
116
|
-
* (both streaming and non-streaming).
|
|
117
|
-
*/
|
|
118
|
-
protected override structuredResponseFormat(
|
|
119
|
-
jsonSchema: StructuredSchema,
|
|
120
|
-
schemaName: string | undefined
|
|
121
|
-
): ChatCompletionCreateParamsNonStreaming["response_format"] {
|
|
122
|
-
return {
|
|
123
|
-
type: "json_schema" as const,
|
|
124
|
-
json_schema: {
|
|
125
|
-
name: schemaName || "structured_output",
|
|
126
|
-
schema: this.adaptSchema(jsonSchema),
|
|
127
|
-
},
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
99
|
/**
|
|
132
100
|
* DeepSeek reports usage in streaming chunks when explicitly requested.
|
|
133
101
|
*/
|
|
@@ -21,7 +21,8 @@ import type {
|
|
|
21
21
|
} from "../types";
|
|
22
22
|
import type { ProviderCapabilities } from "../types/ai";
|
|
23
23
|
import type { HistoryItem } from "../types/history";
|
|
24
|
-
import { withTimeoutAndRetry } from "../utils/retry";
|
|
24
|
+
import { withTimeoutAndRetry, withStreamRetry, resolveRetryConfig, combineAbortSignals } from "../utils/retry";
|
|
25
|
+
import { assertUsableCompletion } from "../utils/completion";
|
|
25
26
|
import { tryParseJSONResponse } from "../utils/json";
|
|
26
27
|
import { logger } from "../utils/logger";
|
|
27
28
|
import {
|
|
@@ -32,11 +33,6 @@ import {
|
|
|
32
33
|
type ErrorClassificationOptions,
|
|
33
34
|
} from "./errorClassification";
|
|
34
35
|
|
|
35
|
-
const DEFAULT_RETRY_CONFIG = {
|
|
36
|
-
timeout: 60000,
|
|
37
|
-
retries: 3,
|
|
38
|
-
};
|
|
39
|
-
|
|
40
36
|
/**
|
|
41
37
|
* Configuration options for Gemini provider
|
|
42
38
|
* Uses types from @google/genai package
|
|
@@ -106,10 +102,7 @@ export class GeminiProvider implements AiProvider {
|
|
|
106
102
|
this.primaryModel = model;
|
|
107
103
|
this.backupModels = backupModels;
|
|
108
104
|
this.config = config;
|
|
109
|
-
this.retryConfig =
|
|
110
|
-
timeout: retryConfig?.timeout || DEFAULT_RETRY_CONFIG.timeout,
|
|
111
|
-
retries: retryConfig?.retries || DEFAULT_RETRY_CONFIG.retries,
|
|
112
|
-
};
|
|
105
|
+
this.retryConfig = resolveRetryConfig(retryConfig);
|
|
113
106
|
}
|
|
114
107
|
|
|
115
108
|
/**
|
|
@@ -419,7 +412,7 @@ export class GeminiProvider implements AiProvider {
|
|
|
419
412
|
model: string,
|
|
420
413
|
input: GenerateMessageInput<TContext>
|
|
421
414
|
): Promise<GenerateMessageOutput<TStructured>> {
|
|
422
|
-
const operation = async (): Promise<GenerateMessageOutput> => {
|
|
415
|
+
const operation = async (signal: AbortSignal): Promise<GenerateMessageOutput> => {
|
|
423
416
|
// Schema-required: configure response schema
|
|
424
417
|
const configOverride: Partial<GenerateContentConfig> = { ...this.config };
|
|
425
418
|
|
|
@@ -464,12 +457,13 @@ export class GeminiProvider implements AiProvider {
|
|
|
464
457
|
}
|
|
465
458
|
}
|
|
466
459
|
|
|
460
|
+
const reqSignal = combineAbortSignals(input.signal, signal);
|
|
467
461
|
response = await this.genAI.models.generateContent({
|
|
468
462
|
model,
|
|
469
463
|
contents: historyContents,
|
|
470
464
|
config: {
|
|
471
465
|
...configOverride,
|
|
472
|
-
...(
|
|
466
|
+
...(reqSignal ? { abortSignal: reqSignal } : {}),
|
|
473
467
|
},
|
|
474
468
|
});
|
|
475
469
|
} catch (error: unknown) {
|
|
@@ -538,6 +532,11 @@ export class GeminiProvider implements AiProvider {
|
|
|
538
532
|
} as AgentStructuredResponse;
|
|
539
533
|
}
|
|
540
534
|
|
|
535
|
+
// A parsed-but-blank message with no tool calls is as empty as no text;
|
|
536
|
+
// the shared guard throws so withTimeoutAndRetry retries instead of
|
|
537
|
+
// returning {"message":""}.
|
|
538
|
+
assertUsableCompletion(structured, message, toolCalls.length, "Gemini");
|
|
539
|
+
|
|
541
540
|
return {
|
|
542
541
|
message,
|
|
543
542
|
metadata: {
|
|
@@ -566,7 +565,10 @@ export class GeminiProvider implements AiProvider {
|
|
|
566
565
|
): AsyncGenerator<GenerateMessageStreamChunk<TStructured>> {
|
|
567
566
|
// Try primary model first
|
|
568
567
|
try {
|
|
569
|
-
yield*
|
|
568
|
+
yield* withStreamRetry(
|
|
569
|
+
(signal) => this.generateStreamWithModel(this.primaryModel, input, signal),
|
|
570
|
+
{ maxRetries: this.retryConfig.retries, firstChunkTimeoutMs: this.retryConfig.timeout, operationName: `Gemini ${this.primaryModel} stream` }
|
|
571
|
+
);
|
|
570
572
|
} catch (primaryError: unknown) {
|
|
571
573
|
const primaryErrMsg = getErrorMessage(primaryError);
|
|
572
574
|
logger.warn(
|
|
@@ -589,7 +591,10 @@ export class GeminiProvider implements AiProvider {
|
|
|
589
591
|
);
|
|
590
592
|
|
|
591
593
|
try {
|
|
592
|
-
yield*
|
|
594
|
+
yield* withStreamRetry(
|
|
595
|
+
(signal) => this.generateStreamWithModel(backupModel, input, signal),
|
|
596
|
+
{ maxRetries: this.retryConfig.retries, firstChunkTimeoutMs: this.retryConfig.timeout, operationName: `Gemini ${backupModel} stream` }
|
|
597
|
+
);
|
|
593
598
|
logger.debug(`[GEMINI] Backup model ${backupModel} succeeded`);
|
|
594
599
|
return;
|
|
595
600
|
} catch (backupError: unknown) {
|
|
@@ -624,8 +629,11 @@ export class GeminiProvider implements AiProvider {
|
|
|
624
629
|
TStructured = AgentStructuredResponse
|
|
625
630
|
>(
|
|
626
631
|
model: string,
|
|
627
|
-
input: GenerateMessageInput<TContext
|
|
632
|
+
input: GenerateMessageInput<TContext>,
|
|
633
|
+
attemptSignal?: AbortSignal
|
|
628
634
|
): AsyncGenerator<GenerateMessageStreamChunk<TStructured>> {
|
|
635
|
+
// Caller cancellation + the retry helper's per-attempt deadline, as one.
|
|
636
|
+
const reqSignal = combineAbortSignals(input.signal, attemptSignal);
|
|
629
637
|
// Streaming: request JSON if schema provided
|
|
630
638
|
const configOverride: Partial<GenerateContentConfig> = { ...this.config };
|
|
631
639
|
|
|
@@ -675,7 +683,7 @@ export class GeminiProvider implements AiProvider {
|
|
|
675
683
|
contents: historyContents,
|
|
676
684
|
config: {
|
|
677
685
|
...configOverride,
|
|
678
|
-
...(
|
|
686
|
+
...(reqSignal ? { abortSignal: reqSignal } : {}),
|
|
679
687
|
},
|
|
680
688
|
});
|
|
681
689
|
} catch (error: unknown) {
|
|
@@ -693,7 +701,7 @@ export class GeminiProvider implements AiProvider {
|
|
|
693
701
|
}> = [];
|
|
694
702
|
|
|
695
703
|
for await (const chunk of stream) {
|
|
696
|
-
if (
|
|
704
|
+
if (reqSignal?.aborted) break;
|
|
697
705
|
|
|
698
706
|
// Safely extract text — chunk.text can throw when chunk has only function calls
|
|
699
707
|
const delta = this.safeExtractText(chunk);
|
|
@@ -747,6 +755,10 @@ export class GeminiProvider implements AiProvider {
|
|
|
747
755
|
} as AgentStructuredResponse;
|
|
748
756
|
}
|
|
749
757
|
|
|
758
|
+
// Empty-completion guard — same definition as the non-streaming path, so the
|
|
759
|
+
// stream retries / falls back to backup instead of emitting an empty message.
|
|
760
|
+
assertUsableCompletion(structured, accumulated, toolCalls.length, "Gemini");
|
|
761
|
+
|
|
750
762
|
// Yield final chunk
|
|
751
763
|
yield {
|
|
752
764
|
delta: "",
|
|
@@ -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, 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,10 +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
|
-
retries: init.retryConfig?.retries || DEFAULT_RETRY_CONFIG.retries,
|
|
105
|
-
};
|
|
115
|
+
this.retryConfig = resolveRetryConfig(init.retryConfig);
|
|
116
|
+
this.structuredOutput = init.structuredOutput ?? "responses_parse";
|
|
106
117
|
}
|
|
107
118
|
|
|
108
119
|
// ---------------------------------------------------------------------
|
|
@@ -142,22 +153,31 @@ export abstract class OpenAICompatibleProvider implements AiProvider {
|
|
|
142
153
|
input: GenerateMessageInput<unknown>,
|
|
143
154
|
jsonSchema: StructuredSchema
|
|
144
155
|
): Promise<GenerateMessageOutput> {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
+
},
|
|
158
177
|
},
|
|
159
178
|
},
|
|
160
|
-
|
|
179
|
+
{ signal: input.signal }
|
|
180
|
+
);
|
|
161
181
|
|
|
162
182
|
if (!response.output_parsed) {
|
|
163
183
|
throw new Error(`No parsed output returned from ${this.displayName}`);
|
|
@@ -179,15 +199,25 @@ export abstract class OpenAICompatibleProvider implements AiProvider {
|
|
|
179
199
|
}
|
|
180
200
|
|
|
181
201
|
/**
|
|
182
|
-
* response_format applied to chat-completions requests when a JSON
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
* 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.)
|
|
186
207
|
*/
|
|
187
208
|
protected structuredResponseFormat(
|
|
188
|
-
|
|
189
|
-
|
|
209
|
+
jsonSchema: StructuredSchema,
|
|
210
|
+
schemaName: string | undefined
|
|
190
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
|
+
}
|
|
191
221
|
return { type: "json_object" };
|
|
192
222
|
}
|
|
193
223
|
|
|
@@ -408,9 +438,9 @@ export abstract class OpenAICompatibleProvider implements AiProvider {
|
|
|
408
438
|
): AsyncGenerator<GenerateMessageStreamChunk<TStructured>> {
|
|
409
439
|
// Try primary model first
|
|
410
440
|
try {
|
|
411
|
-
yield*
|
|
412
|
-
this.primaryModel,
|
|
413
|
-
|
|
441
|
+
yield* withStreamRetry(
|
|
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` }
|
|
414
444
|
);
|
|
415
445
|
} catch (primaryError: unknown) {
|
|
416
446
|
const primaryErrMsg = getErrorMessage(primaryError);
|
|
@@ -435,9 +465,9 @@ export abstract class OpenAICompatibleProvider implements AiProvider {
|
|
|
435
465
|
);
|
|
436
466
|
|
|
437
467
|
try {
|
|
438
|
-
yield*
|
|
439
|
-
backupModel,
|
|
440
|
-
|
|
468
|
+
yield* withStreamRetry(
|
|
469
|
+
(signal) => this.generateStreamWithModel<TContext, TStructured>(backupModel, input, signal),
|
|
470
|
+
{ maxRetries: this.retryConfig.retries, firstChunkTimeoutMs: this.retryConfig.timeout, operationName: `${this.logLabel} ${backupModel} stream` }
|
|
441
471
|
);
|
|
442
472
|
logger.debug(`[${this.logLabel}] Backup model ${backupModel} succeeded`);
|
|
443
473
|
return;
|
|
@@ -479,16 +509,20 @@ export abstract class OpenAICompatibleProvider implements AiProvider {
|
|
|
479
509
|
model: string,
|
|
480
510
|
input: GenerateMessageInput<TContext>
|
|
481
511
|
): Promise<GenerateMessageOutput<TStructured>> {
|
|
482
|
-
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) };
|
|
483
517
|
// Use structured output path if JSON schema is provided
|
|
484
|
-
if (
|
|
518
|
+
if (reqInput.parameters?.jsonSchema) {
|
|
485
519
|
return this.executeStructuredGenerate(
|
|
486
520
|
model,
|
|
487
|
-
|
|
488
|
-
|
|
521
|
+
reqInput,
|
|
522
|
+
reqInput.parameters.jsonSchema
|
|
489
523
|
);
|
|
490
524
|
}
|
|
491
|
-
return this.executeChatCompletion(model,
|
|
525
|
+
return this.executeChatCompletion(model, reqInput);
|
|
492
526
|
};
|
|
493
527
|
|
|
494
528
|
return withTimeoutAndRetry(
|
|
@@ -536,7 +570,9 @@ export abstract class OpenAICompatibleProvider implements AiProvider {
|
|
|
536
570
|
);
|
|
537
571
|
}
|
|
538
572
|
|
|
539
|
-
const response = await this.client.chat.completions.create(params
|
|
573
|
+
const response = await this.client.chat.completions.create(params, {
|
|
574
|
+
signal: input.signal,
|
|
575
|
+
});
|
|
540
576
|
|
|
541
577
|
const message = response.choices[0]?.message?.content || "";
|
|
542
578
|
|
|
@@ -580,6 +616,11 @@ export abstract class OpenAICompatibleProvider implements AiProvider {
|
|
|
580
616
|
} as AgentStructuredResponse;
|
|
581
617
|
}
|
|
582
618
|
|
|
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
|
|
621
|
+
// returning {"message":""}.
|
|
622
|
+
assertUsableCompletion(structured, message, toolCalls.length, this.displayName);
|
|
623
|
+
|
|
583
624
|
return {
|
|
584
625
|
message,
|
|
585
626
|
metadata: {
|
|
@@ -602,7 +643,8 @@ export abstract class OpenAICompatibleProvider implements AiProvider {
|
|
|
602
643
|
TStructured = AgentStructuredResponse
|
|
603
644
|
>(
|
|
604
645
|
model: string,
|
|
605
|
-
input: GenerateMessageInput<TContext
|
|
646
|
+
input: GenerateMessageInput<TContext>,
|
|
647
|
+
attemptSignal?: AbortSignal
|
|
606
648
|
): AsyncGenerator<GenerateMessageStreamChunk<TStructured>> {
|
|
607
649
|
// Build messages from history and append prompt as final user message
|
|
608
650
|
const historyMessages = this.buildChatMessages(input.history);
|
|
@@ -637,7 +679,9 @@ export abstract class OpenAICompatibleProvider implements AiProvider {
|
|
|
637
679
|
);
|
|
638
680
|
}
|
|
639
681
|
|
|
640
|
-
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
|
+
});
|
|
641
685
|
|
|
642
686
|
let accumulated = "";
|
|
643
687
|
let currentModel = model;
|
|
@@ -720,6 +764,10 @@ export abstract class OpenAICompatibleProvider implements AiProvider {
|
|
|
720
764
|
} as TStructured;
|
|
721
765
|
}
|
|
722
766
|
|
|
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);
|
|
770
|
+
|
|
723
771
|
// Yield final chunk
|
|
724
772
|
yield {
|
|
725
773
|
delta: "",
|
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 } from "./retry";
|
|
59
|
-
export { retry, withTimeoutAndRetry } 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 {
|