@plumbus/core 0.2.9 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/ai/__tests__/ai-service.test.js +704 -1
- package/dist/ai/__tests__/ai-service.test.js.map +1 -1
- package/dist/ai/__tests__/chunking.test.js +34 -0
- package/dist/ai/__tests__/chunking.test.js.map +1 -1
- package/dist/ai/__tests__/model-pricing.test.js +45 -1
- package/dist/ai/__tests__/model-pricing.test.js.map +1 -1
- package/dist/ai/__tests__/provider.test.d.ts.map +1 -1
- package/dist/ai/__tests__/provider.test.js +730 -1
- package/dist/ai/__tests__/provider.test.js.map +1 -1
- package/dist/ai/__tests__/validation.test.js +200 -1
- package/dist/ai/__tests__/validation.test.js.map +1 -1
- package/dist/ai/__tests__/zod-to-provider-schema.test.d.ts +2 -0
- package/dist/ai/__tests__/zod-to-provider-schema.test.d.ts.map +1 -0
- package/dist/ai/__tests__/zod-to-provider-schema.test.js +60 -0
- package/dist/ai/__tests__/zod-to-provider-schema.test.js.map +1 -0
- package/dist/ai/ai-service.d.ts +19 -2
- package/dist/ai/ai-service.d.ts.map +1 -1
- package/dist/ai/ai-service.js +544 -110
- package/dist/ai/ai-service.js.map +1 -1
- package/dist/ai/cost-tracker.d.ts +26 -1
- package/dist/ai/cost-tracker.d.ts.map +1 -1
- package/dist/ai/cost-tracker.js +1 -0
- package/dist/ai/cost-tracker.js.map +1 -1
- package/dist/ai/index.d.ts +6 -4
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js +7 -3
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/model-pricing.d.ts +24 -1
- package/dist/ai/model-pricing.d.ts.map +1 -1
- package/dist/ai/model-pricing.js +83 -56
- package/dist/ai/model-pricing.js.map +1 -1
- package/dist/ai/provider.d.ts +110 -1
- package/dist/ai/provider.d.ts.map +1 -1
- package/dist/ai/provider.js +358 -32
- package/dist/ai/provider.js.map +1 -1
- package/dist/ai/rag/chunking.js +53 -3
- package/dist/ai/rag/chunking.js.map +1 -1
- package/dist/ai/refusal.d.ts +28 -0
- package/dist/ai/refusal.d.ts.map +1 -0
- package/dist/ai/refusal.js +36 -0
- package/dist/ai/refusal.js.map +1 -0
- package/dist/ai/validation.d.ts +40 -8
- package/dist/ai/validation.d.ts.map +1 -1
- package/dist/ai/validation.js +97 -6
- package/dist/ai/validation.js.map +1 -1
- package/dist/ai/zod-to-provider-schema.d.ts +15 -0
- package/dist/ai/zod-to-provider-schema.d.ts.map +1 -0
- package/dist/ai/zod-to-provider-schema.js +200 -0
- package/dist/ai/zod-to-provider-schema.js.map +1 -0
- package/dist/cli/__tests__/doctor.test.js +104 -2
- package/dist/cli/__tests__/doctor.test.js.map +1 -1
- package/dist/cli/__tests__/init.test.js +127 -7
- package/dist/cli/__tests__/init.test.js.map +1 -1
- package/dist/cli/__tests__/migrate.test.js +159 -2
- package/dist/cli/__tests__/migrate.test.js.map +1 -1
- package/dist/cli/commands/__tests__/commands.test.js +5 -0
- package/dist/cli/commands/__tests__/commands.test.js.map +1 -1
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +21 -1
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +78 -0
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +7 -2
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/init.d.ts +16 -2
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +226 -25
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/migrate.d.ts.map +1 -1
- package/dist/cli/commands/migrate.js +258 -8
- package/dist/cli/commands/migrate.js.map +1 -1
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +21 -1
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/data/__tests__/drift-inspector.test.d.ts +2 -0
- package/dist/data/__tests__/drift-inspector.test.d.ts.map +1 -0
- package/dist/data/__tests__/drift-inspector.test.js +170 -0
- package/dist/data/__tests__/drift-inspector.test.js.map +1 -0
- package/dist/data/__tests__/framework-migration-rewriter.test.d.ts +2 -0
- package/dist/data/__tests__/framework-migration-rewriter.test.d.ts.map +1 -0
- package/dist/data/__tests__/framework-migration-rewriter.test.js +329 -0
- package/dist/data/__tests__/framework-migration-rewriter.test.js.map +1 -0
- package/dist/data/__tests__/migration.test.js +93 -2
- package/dist/data/__tests__/migration.test.js.map +1 -1
- package/dist/data/__tests__/repository.test.js +126 -0
- package/dist/data/__tests__/repository.test.js.map +1 -1
- package/dist/data/drift-inspector.d.ts +41 -0
- package/dist/data/drift-inspector.d.ts.map +1 -0
- package/dist/data/drift-inspector.js +197 -0
- package/dist/data/drift-inspector.js.map +1 -0
- package/dist/data/framework-migration-rewriter.d.ts +61 -0
- package/dist/data/framework-migration-rewriter.d.ts.map +1 -0
- package/dist/data/framework-migration-rewriter.js +357 -0
- package/dist/data/framework-migration-rewriter.js.map +1 -0
- package/dist/data/index.d.ts +2 -1
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +2 -1
- package/dist/data/index.js.map +1 -1
- package/dist/data/migration.d.ts +31 -1
- package/dist/data/migration.d.ts.map +1 -1
- package/dist/data/migration.js +113 -3
- package/dist/data/migration.js.map +1 -1
- package/dist/data/repository.d.ts.map +1 -1
- package/dist/data/repository.js +21 -0
- package/dist/data/repository.js.map +1 -1
- package/dist/define/__tests__/definePrompt.test.js +37 -0
- package/dist/define/__tests__/definePrompt.test.js.map +1 -1
- package/dist/define/definePrompt.d.ts +13 -0
- package/dist/define/definePrompt.d.ts.map +1 -1
- package/dist/define/definePrompt.js +3 -0
- package/dist/define/definePrompt.js.map +1 -1
- package/dist/errors/http.d.ts.map +1 -1
- package/dist/errors/http.js +2 -0
- package/dist/errors/http.js.map +1 -1
- package/dist/errors/index.d.ts +12 -0
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +21 -0
- package/dist/errors/index.js.map +1 -1
- package/dist/events/__tests__/event-emitter.test.js +72 -0
- package/dist/events/__tests__/event-emitter.test.js.map +1 -1
- package/dist/events/emitter.d.ts.map +1 -1
- package/dist/events/emitter.js +51 -0
- package/dist/events/emitter.js.map +1 -1
- package/dist/execution/__tests__/context-factory.test.js +1 -0
- package/dist/execution/__tests__/context-factory.test.js.map +1 -1
- package/dist/execution/context-factory.d.ts.map +1 -1
- package/dist/execution/context-factory.js +2 -0
- package/dist/execution/context-factory.js.map +1 -1
- package/dist/flows/__tests__/flow-engine.test.js +451 -4
- package/dist/flows/__tests__/flow-engine.test.js.map +1 -1
- package/dist/flows/__tests__/flow-service.test.js +8 -0
- package/dist/flows/__tests__/flow-service.test.js.map +1 -1
- package/dist/flows/__tests__/step-executor.test.js +8 -2
- package/dist/flows/__tests__/step-executor.test.js.map +1 -1
- package/dist/flows/engine.d.ts +50 -1
- package/dist/flows/engine.d.ts.map +1 -1
- package/dist/flows/engine.js +427 -23
- package/dist/flows/engine.js.map +1 -1
- package/dist/flows/flow-service.d.ts.map +1 -1
- package/dist/flows/flow-service.js +4 -0
- package/dist/flows/flow-service.js.map +1 -1
- package/dist/flows/index.d.ts +1 -1
- package/dist/flows/index.d.ts.map +1 -1
- package/dist/flows/index.js +1 -1
- package/dist/flows/index.js.map +1 -1
- package/dist/flows/schema.d.ts +34 -0
- package/dist/flows/schema.d.ts.map +1 -1
- package/dist/flows/schema.js +3 -0
- package/dist/flows/schema.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/server/bootstrap.d.ts +14 -0
- package/dist/server/bootstrap.d.ts.map +1 -1
- package/dist/server/bootstrap.js +23 -4
- package/dist/server/bootstrap.js.map +1 -1
- package/dist/testing/context.d.ts.map +1 -1
- package/dist/testing/context.js +37 -1
- package/dist/testing/context.js.map +1 -1
- package/dist/types/context.d.ts +110 -0
- package/dist/types/context.d.ts.map +1 -1
- package/dist/types/enums.d.ts +2 -0
- package/dist/types/enums.d.ts.map +1 -1
- package/dist/types/enums.js +2 -0
- package/dist/types/enums.js.map +1 -1
- package/dist/types/prompt.d.ts +64 -0
- package/dist/types/prompt.d.ts.map +1 -1
- package/dist/worker/__tests__/bootstrap.test.js +36 -2
- package/dist/worker/__tests__/bootstrap.test.js.map +1 -1
- package/dist/worker/bootstrap.d.ts +7 -0
- package/dist/worker/bootstrap.d.ts.map +1 -1
- package/dist/worker/bootstrap.js +153 -12
- package/dist/worker/bootstrap.js.map +1 -1
- package/instructions/framework.md +7 -0
- package/instructions/guardrails.md +39 -0
- package/instructions/patterns.md +10 -0
- package/package.json +3 -2
package/dist/ai/provider.d.ts
CHANGED
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
import type { AIProviderConfig } from '../types/config.js';
|
|
2
|
+
import { type Kind } from './model-pricing.js';
|
|
3
|
+
/**
|
|
4
|
+
* A single chat-history turn. Used for native multi-turn requests where the
|
|
5
|
+
* model should see real conversation flow (assistant + user alternation).
|
|
6
|
+
* Optional — when omitted, providers fall back to single-user-message mode
|
|
7
|
+
* built from `ProviderRequest.prompt`.
|
|
8
|
+
*/
|
|
9
|
+
export interface ChatMessage {
|
|
10
|
+
role: 'user' | 'assistant';
|
|
11
|
+
content: string;
|
|
12
|
+
}
|
|
2
13
|
export interface ProviderRequest {
|
|
3
14
|
/** System prompt / instructions */
|
|
4
15
|
system?: string;
|
|
5
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* User prompt content. Used as the SOLE user message when `messages` is
|
|
18
|
+
* absent. When `messages` is provided, this field is IGNORED — the messages
|
|
19
|
+
* array is the conversation. Callers passing `messages` may still set
|
|
20
|
+
* `prompt: ''` for type-compat.
|
|
21
|
+
*/
|
|
6
22
|
prompt: string;
|
|
23
|
+
/**
|
|
24
|
+
* Optional native multi-turn conversation history. When present, providers
|
|
25
|
+
* send `[system?, ...messages]` to the API instead of `[system?, {role:'user', content: prompt}]`.
|
|
26
|
+
* The LAST entry should be a `user` message (the latest turn). Order is
|
|
27
|
+
* preserved exactly as supplied. Empty array is treated as "no messages"
|
|
28
|
+
* (falls back to single-user mode using `prompt`).
|
|
29
|
+
*/
|
|
30
|
+
messages?: ChatMessage[];
|
|
7
31
|
/** Model name override */
|
|
8
32
|
model?: string;
|
|
9
33
|
/** Temperature (0-2) */
|
|
@@ -12,9 +36,30 @@ export interface ProviderRequest {
|
|
|
12
36
|
maxTokens?: number;
|
|
13
37
|
/** Response format hint */
|
|
14
38
|
responseFormat?: 'text' | 'json';
|
|
39
|
+
/** Provider-compatible JSON Schema for strict structured outputs. */
|
|
40
|
+
responseSchema?: Record<string, unknown>;
|
|
41
|
+
/** Transport used for provider-side structured outputs. Defaults to response_format. */
|
|
42
|
+
structuredOutputTransport?: 'response_format' | 'tool';
|
|
15
43
|
/** Request timeout in milliseconds (default: 120_000) */
|
|
16
44
|
timeout?: number;
|
|
45
|
+
/**
|
|
46
|
+
* External AbortSignal (e.g. `ctx.signal`). When fired, the in-flight
|
|
47
|
+
* fetch(es) for this request abort immediately. Combined with the internal
|
|
48
|
+
* timeout signal via `AbortSignal.any([...])`, so whichever triggers first
|
|
49
|
+
* wins.
|
|
50
|
+
*/
|
|
51
|
+
signal?: AbortSignal;
|
|
52
|
+
/**
|
|
53
|
+
* Deterministic sampling seed. OpenAI-compatible providers (including xAI
|
|
54
|
+
* Grok) honor this parameter: identical `{seed, temperature, model, prompt}`
|
|
55
|
+
* tuples produce the same tokens. Combined with `temperature: 0`, lets
|
|
56
|
+
* callers pin reproducible output for structural-planning prompts. Ignored
|
|
57
|
+
* by providers that do not support it.
|
|
58
|
+
*/
|
|
59
|
+
seed?: number;
|
|
17
60
|
}
|
|
61
|
+
/** Compose a request's optional user signal with a timeout, returning a single AbortSignal. */
|
|
62
|
+
export declare function combineRequestSignals(userSignal: AbortSignal | undefined, timeoutMs: number): AbortSignal;
|
|
18
63
|
export interface ProviderResponse {
|
|
19
64
|
content: string;
|
|
20
65
|
model: string;
|
|
@@ -52,6 +97,40 @@ export interface EmbeddingResponse {
|
|
|
52
97
|
totalTokens: number;
|
|
53
98
|
};
|
|
54
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* A single model entry returned by `AIProviderAdapter.listModels()`.
|
|
102
|
+
* The id is whatever the provider's `/v1/models` returns; pricing and `kind`
|
|
103
|
+
* are joined from the framework's pricing catalog. Models not in the catalog
|
|
104
|
+
* surface as `kind: 'unknown'` with `null` prices.
|
|
105
|
+
*/
|
|
106
|
+
export interface ProviderModel {
|
|
107
|
+
/** API identifier (the value to pass as `model` in requests). */
|
|
108
|
+
id: string;
|
|
109
|
+
/** Adapter name (e.g. "openai", "anthropic", or a custom adapter's name). */
|
|
110
|
+
provider: string;
|
|
111
|
+
/** Capability classification, from the pricing catalog. `'unknown'` if not in the catalog. */
|
|
112
|
+
kind: Kind | 'unknown';
|
|
113
|
+
/** USD per 1M input tokens, or `null` if the model is not in the catalog. */
|
|
114
|
+
inputPerMTok: number | null;
|
|
115
|
+
/** USD per 1M output tokens, or `null` if the model is not in the catalog. */
|
|
116
|
+
outputPerMTok: number | null;
|
|
117
|
+
/** Provider-supplied human-readable label (Anthropic exposes this; OpenAI doesn't). */
|
|
118
|
+
displayName?: string;
|
|
119
|
+
/** ISO-8601 creation timestamp when the provider exposes one. */
|
|
120
|
+
createdAt?: string;
|
|
121
|
+
/** Owner/team (OpenAI exposes this; Anthropic doesn't). */
|
|
122
|
+
ownedBy?: string;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Optional filter passed to `AIProviderAdapter.listModels()`. `kind` accepts a
|
|
126
|
+
* single Kind or an array. When unset, all models are returned (including
|
|
127
|
+
* unknown ones). When set against an official endpoint, unknown-kind models
|
|
128
|
+
* are excluded; against a custom endpoint they are included alongside the
|
|
129
|
+
* filtered matches (see adapter docs).
|
|
130
|
+
*/
|
|
131
|
+
export interface ListModelsFilter {
|
|
132
|
+
kind?: Kind | Kind[];
|
|
133
|
+
}
|
|
55
134
|
export interface AIProviderAdapter {
|
|
56
135
|
readonly name: string;
|
|
57
136
|
/** Send a completion request */
|
|
@@ -60,7 +139,37 @@ export interface AIProviderAdapter {
|
|
|
60
139
|
stream(request: ProviderRequest): AsyncIterable<ProviderStreamEvent>;
|
|
61
140
|
/** Generate embeddings for texts */
|
|
62
141
|
embed(request: EmbeddingRequest): Promise<EmbeddingResponse>;
|
|
142
|
+
/**
|
|
143
|
+
* List models the provider exposes for this account, joined against the
|
|
144
|
+
* framework's pricing catalog. Optional — custom adapters that don't
|
|
145
|
+
* implement it still satisfy the interface.
|
|
146
|
+
*
|
|
147
|
+
* Filtering rule (see docs for full details):
|
|
148
|
+
* - No filter → all models, including unknown-kind entries.
|
|
149
|
+
* - Filter + official endpoint → only matching kinds; unknowns excluded.
|
|
150
|
+
* - Filter + custom endpoint → matching kinds **plus** all unknowns.
|
|
151
|
+
*
|
|
152
|
+
* On network error / 404 / unsupported endpoint, returns `[]` and emits
|
|
153
|
+
* one `console.warn`; does not throw.
|
|
154
|
+
*/
|
|
155
|
+
listModels?(filter?: ListModelsFilter): Promise<ProviderModel[]>;
|
|
63
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Shared logic used by adapter `listModels` implementations: join a list of
|
|
159
|
+
* provider-supplied model entries against the pricing catalog, then apply
|
|
160
|
+
* the kind filter respecting the official-endpoint rule.
|
|
161
|
+
*/
|
|
162
|
+
export declare function joinAndFilterModels(args: {
|
|
163
|
+
provider: string;
|
|
164
|
+
entries: Array<{
|
|
165
|
+
id: string;
|
|
166
|
+
displayName?: string;
|
|
167
|
+
createdAt?: string;
|
|
168
|
+
ownedBy?: string;
|
|
169
|
+
}>;
|
|
170
|
+
filter: ListModelsFilter | undefined;
|
|
171
|
+
isOfficial: boolean;
|
|
172
|
+
}): ProviderModel[];
|
|
64
173
|
export declare class ProviderAPIError extends Error {
|
|
65
174
|
readonly providerName: string;
|
|
66
175
|
readonly statusCode?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/ai/provider.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/ai/provider.ts"],"names":[],"mappings":"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"}
|
package/dist/ai/provider.js
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
// ── AI Provider Adapter Interface ──
|
|
2
2
|
// Abstract interface for AI provider adapters (OpenAI, Anthropic, etc.)
|
|
3
|
+
import { allKnownModels } from './model-pricing.js';
|
|
4
|
+
import { AIIncompleteOutputError, AIRefusalError } from './refusal.js';
|
|
5
|
+
/** Compose a request's optional user signal with a timeout, returning a single AbortSignal. */
|
|
6
|
+
export function combineRequestSignals(userSignal, timeoutMs) {
|
|
7
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
8
|
+
if (!userSignal)
|
|
9
|
+
return timeoutSignal;
|
|
10
|
+
return AbortSignal.any([userSignal, timeoutSignal]);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Shared logic used by adapter `listModels` implementations: join a list of
|
|
14
|
+
* provider-supplied model entries against the pricing catalog, then apply
|
|
15
|
+
* the kind filter respecting the official-endpoint rule.
|
|
16
|
+
*/
|
|
17
|
+
export function joinAndFilterModels(args) {
|
|
18
|
+
const catalog = new Map(allKnownModels()
|
|
19
|
+
.filter(([, rate]) => rate.kind != null)
|
|
20
|
+
.map(([id, rate]) => [
|
|
21
|
+
id,
|
|
22
|
+
{
|
|
23
|
+
kind: rate.kind,
|
|
24
|
+
inputPerMTok: rate.inputPerMTok,
|
|
25
|
+
outputPerMTok: rate.outputPerMTok,
|
|
26
|
+
},
|
|
27
|
+
]));
|
|
28
|
+
const joined = args.entries.map((e) => {
|
|
29
|
+
const cat = catalog.get(e.id);
|
|
30
|
+
const model = {
|
|
31
|
+
id: e.id,
|
|
32
|
+
provider: args.provider,
|
|
33
|
+
kind: cat?.kind ?? 'unknown',
|
|
34
|
+
inputPerMTok: cat?.inputPerMTok ?? null,
|
|
35
|
+
outputPerMTok: cat?.outputPerMTok ?? null,
|
|
36
|
+
};
|
|
37
|
+
if (e.displayName != null)
|
|
38
|
+
model.displayName = e.displayName;
|
|
39
|
+
if (e.createdAt != null)
|
|
40
|
+
model.createdAt = e.createdAt;
|
|
41
|
+
if (e.ownedBy != null)
|
|
42
|
+
model.ownedBy = e.ownedBy;
|
|
43
|
+
return model;
|
|
44
|
+
});
|
|
45
|
+
if (!args.filter?.kind)
|
|
46
|
+
return joined;
|
|
47
|
+
const allowedKinds = new Set(Array.isArray(args.filter.kind) ? args.filter.kind : [args.filter.kind]);
|
|
48
|
+
return joined.filter((m) => {
|
|
49
|
+
if (m.kind === 'unknown')
|
|
50
|
+
return !args.isOfficial;
|
|
51
|
+
return allowedKinds.has(m.kind);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
3
54
|
export class ProviderAPIError extends Error {
|
|
4
55
|
providerName;
|
|
5
56
|
statusCode;
|
|
@@ -52,6 +103,89 @@ function computeRetryDelayMs(attempt, retryAfterHeader) {
|
|
|
52
103
|
}
|
|
53
104
|
return Math.min(baseRetryDelayMs * 2 ** (attempt - 1), maxRetryDelayMs);
|
|
54
105
|
}
|
|
106
|
+
function buildOpenAIResponseFormat(request) {
|
|
107
|
+
if (request.responseFormat !== 'json')
|
|
108
|
+
return undefined;
|
|
109
|
+
if (!request.responseSchema)
|
|
110
|
+
return { type: 'json_object' };
|
|
111
|
+
return {
|
|
112
|
+
type: 'json_schema',
|
|
113
|
+
json_schema: {
|
|
114
|
+
name: 'response',
|
|
115
|
+
strict: true,
|
|
116
|
+
schema: request.responseSchema,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const OPENAI_STRUCTURED_OUTPUT_TOOL_NAME = 'return_structured_response';
|
|
121
|
+
function shouldUseOpenAIStructuredTool(args) {
|
|
122
|
+
return (args.request.structuredOutputTransport === 'tool' &&
|
|
123
|
+
args.request.responseFormat === 'json' &&
|
|
124
|
+
!!args.request.responseSchema);
|
|
125
|
+
}
|
|
126
|
+
function buildOpenAIStructuredOutputTool(schema) {
|
|
127
|
+
return {
|
|
128
|
+
type: 'function',
|
|
129
|
+
function: {
|
|
130
|
+
name: OPENAI_STRUCTURED_OUTPUT_TOOL_NAME,
|
|
131
|
+
description: 'Return the complete structured response for this request.',
|
|
132
|
+
strict: true,
|
|
133
|
+
parameters: schema,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function extractOpenAIMessageContent(message) {
|
|
138
|
+
const structuredToolCall = message.tool_calls?.find((toolCall) => toolCall.function?.name === OPENAI_STRUCTURED_OUTPUT_TOOL_NAME);
|
|
139
|
+
const toolArguments = structuredToolCall?.function?.arguments;
|
|
140
|
+
if (typeof toolArguments === 'string' && toolArguments.length > 0) {
|
|
141
|
+
return toolArguments;
|
|
142
|
+
}
|
|
143
|
+
return message.content ?? '';
|
|
144
|
+
}
|
|
145
|
+
function buildEmptyOpenAIStructuredContentError(args) {
|
|
146
|
+
const messageKeys = Object.keys(args.choice.message).join(',');
|
|
147
|
+
const toolCallSummary = args.choice.message.tool_calls
|
|
148
|
+
?.map((toolCall) => {
|
|
149
|
+
const name = toolCall.function?.name ?? 'unknown';
|
|
150
|
+
const argumentLength = toolCall.function?.arguments?.length ?? 0;
|
|
151
|
+
return `${name}:argumentsChars=${argumentLength}`;
|
|
152
|
+
})
|
|
153
|
+
.join('|') ?? 'none';
|
|
154
|
+
return new Error(`OpenAI-compatible provider returned empty JSON response content ` +
|
|
155
|
+
`finish_reason=${args.choice.finish_reason} messageKeys=${messageKeys || 'none'} ` +
|
|
156
|
+
`hasRefusal=${args.choice.message.refusal ? 'true' : 'false'} ` +
|
|
157
|
+
`toolCalls=${toolCallSummary}`);
|
|
158
|
+
}
|
|
159
|
+
function openAIUsageToTokenUsage(usage) {
|
|
160
|
+
return {
|
|
161
|
+
inputTokens: usage.prompt_tokens,
|
|
162
|
+
outputTokens: usage.completion_tokens,
|
|
163
|
+
totalTokens: usage.total_tokens,
|
|
164
|
+
cachedInputTokens: usage.prompt_tokens_details?.cached_tokens ?? 0,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function shouldUseOpenAIMaxCompletionTokens(model) {
|
|
168
|
+
const normalizedModel = model.trim().toLowerCase();
|
|
169
|
+
return /^(?:o\d|o\d-|o\d\.)/.test(normalizedModel) || normalizedModel.startsWith('gpt-5');
|
|
170
|
+
}
|
|
171
|
+
function applyOpenAITokenLimit(body, model, maxTokens) {
|
|
172
|
+
if (!maxTokens)
|
|
173
|
+
return;
|
|
174
|
+
if (shouldUseOpenAIMaxCompletionTokens(model)) {
|
|
175
|
+
body.max_completion_tokens = maxTokens;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
body.max_tokens = maxTokens;
|
|
179
|
+
}
|
|
180
|
+
function anthropicUsageToTokenUsage(usage) {
|
|
181
|
+
return {
|
|
182
|
+
inputTokens: usage.input_tokens,
|
|
183
|
+
outputTokens: usage.output_tokens,
|
|
184
|
+
totalTokens: usage.input_tokens + usage.output_tokens,
|
|
185
|
+
cachedInputTokens: usage.cache_read_input_tokens ?? 0,
|
|
186
|
+
cacheWriteTokens: usage.cache_creation_input_tokens ?? 0,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
55
189
|
async function waitForRetry(delayMs) {
|
|
56
190
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
57
191
|
}
|
|
@@ -94,20 +228,46 @@ export function createOpenAIAdapter(config) {
|
|
|
94
228
|
return {
|
|
95
229
|
name: 'openai',
|
|
96
230
|
async complete(request) {
|
|
231
|
+
const model = request.model ?? defaultModel;
|
|
97
232
|
const messages = [];
|
|
98
233
|
if (request.system) {
|
|
99
234
|
messages.push({ role: 'system', content: request.system });
|
|
100
235
|
}
|
|
101
|
-
|
|
236
|
+
// Multi-turn path: when `messages` is supplied, append them verbatim
|
|
237
|
+
// (preserving role + order). Single-turn path (legacy): build a single
|
|
238
|
+
// user message from `prompt`.
|
|
239
|
+
if (request.messages && request.messages.length > 0) {
|
|
240
|
+
for (const msg of request.messages) {
|
|
241
|
+
messages.push({ role: msg.role, content: msg.content });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
messages.push({ role: 'user', content: request.prompt });
|
|
246
|
+
}
|
|
102
247
|
const body = {
|
|
103
|
-
model
|
|
248
|
+
model,
|
|
104
249
|
messages,
|
|
105
250
|
temperature: request.temperature ?? 0.7,
|
|
106
251
|
};
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
252
|
+
applyOpenAITokenLimit(body, model, request.maxTokens);
|
|
253
|
+
const structuredToolSchema = shouldUseOpenAIStructuredTool({ request })
|
|
254
|
+
? request.responseSchema
|
|
255
|
+
: undefined;
|
|
256
|
+
if (structuredToolSchema) {
|
|
257
|
+
body.tools = [buildOpenAIStructuredOutputTool(structuredToolSchema)];
|
|
258
|
+
body.tool_choice = {
|
|
259
|
+
type: 'function',
|
|
260
|
+
function: { name: OPENAI_STRUCTURED_OUTPUT_TOOL_NAME },
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
const responseFormat = buildOpenAIResponseFormat(request);
|
|
265
|
+
if (responseFormat) {
|
|
266
|
+
body.response_format = responseFormat;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (typeof request.seed === 'number') {
|
|
270
|
+
body.seed = request.seed;
|
|
111
271
|
}
|
|
112
272
|
const resp = await executeRequestWithRetry({
|
|
113
273
|
providerName: 'openai',
|
|
@@ -119,42 +279,77 @@ export function createOpenAIAdapter(config) {
|
|
|
119
279
|
Authorization: `Bearer ${config.apiKey}`,
|
|
120
280
|
},
|
|
121
281
|
body: JSON.stringify(body),
|
|
122
|
-
signal:
|
|
282
|
+
signal: combineRequestSignals(request.signal, request.timeout ?? defaultTimeout),
|
|
123
283
|
}),
|
|
124
284
|
});
|
|
125
285
|
const data = (await resp.json());
|
|
126
286
|
const choice = data.choices[0];
|
|
127
287
|
if (!choice)
|
|
128
288
|
throw new Error('OpenAI returned no choices');
|
|
289
|
+
const usage = openAIUsageToTokenUsage(data.usage);
|
|
290
|
+
const content = extractOpenAIMessageContent(choice.message);
|
|
291
|
+
if (choice.message.refusal) {
|
|
292
|
+
throw new AIRefusalError({
|
|
293
|
+
provider: 'openai',
|
|
294
|
+
model: data.model,
|
|
295
|
+
refusalText: choice.message.refusal,
|
|
296
|
+
usage,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
// Only treat `finish_reason === 'length'` as a hard failure when the
|
|
300
|
+
// caller asked for structured JSON output. For free-text generation,
|
|
301
|
+
// a `length` finish is a partial-success: return what we have rather
|
|
302
|
+
// than discarding tokens the caller already paid for.
|
|
303
|
+
if (choice.finish_reason === 'length' &&
|
|
304
|
+
(request.responseFormat === 'json' || !!request.responseSchema)) {
|
|
305
|
+
throw new AIIncompleteOutputError({
|
|
306
|
+
provider: 'openai',
|
|
307
|
+
model: data.model,
|
|
308
|
+
partialText: content,
|
|
309
|
+
usage,
|
|
310
|
+
finishReason: choice.finish_reason,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
if (request.responseFormat === 'json' && content.trim().length === 0) {
|
|
314
|
+
throw buildEmptyOpenAIStructuredContentError({ data, choice });
|
|
315
|
+
}
|
|
129
316
|
return {
|
|
130
|
-
content
|
|
317
|
+
content,
|
|
131
318
|
model: data.model,
|
|
132
|
-
usage
|
|
133
|
-
inputTokens: data.usage.prompt_tokens,
|
|
134
|
-
outputTokens: data.usage.completion_tokens,
|
|
135
|
-
totalTokens: data.usage.total_tokens,
|
|
136
|
-
cachedInputTokens: data.usage.prompt_tokens_details?.cached_tokens ?? 0,
|
|
137
|
-
},
|
|
319
|
+
usage,
|
|
138
320
|
finishReason: choice.finish_reason,
|
|
139
321
|
};
|
|
140
322
|
},
|
|
141
323
|
async *stream(request) {
|
|
324
|
+
const model = request.model ?? defaultModel;
|
|
142
325
|
const messages = [];
|
|
143
326
|
if (request.system) {
|
|
144
327
|
messages.push({ role: 'system', content: request.system });
|
|
145
328
|
}
|
|
146
|
-
|
|
329
|
+
// Multi-turn path: when `messages` is supplied, append them verbatim.
|
|
330
|
+
// Single-turn path (legacy): single user message from `prompt`.
|
|
331
|
+
if (request.messages && request.messages.length > 0) {
|
|
332
|
+
for (const msg of request.messages) {
|
|
333
|
+
messages.push({ role: msg.role, content: msg.content });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
messages.push({ role: 'user', content: request.prompt });
|
|
338
|
+
}
|
|
147
339
|
const body = {
|
|
148
|
-
model
|
|
340
|
+
model,
|
|
149
341
|
messages,
|
|
150
342
|
temperature: request.temperature ?? 0.7,
|
|
151
343
|
stream: true,
|
|
152
344
|
stream_options: { include_usage: true },
|
|
153
345
|
};
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (
|
|
157
|
-
body.response_format =
|
|
346
|
+
applyOpenAITokenLimit(body, model, request.maxTokens);
|
|
347
|
+
const responseFormat = buildOpenAIResponseFormat(request);
|
|
348
|
+
if (responseFormat) {
|
|
349
|
+
body.response_format = responseFormat;
|
|
350
|
+
}
|
|
351
|
+
if (typeof request.seed === 'number') {
|
|
352
|
+
body.seed = request.seed;
|
|
158
353
|
}
|
|
159
354
|
let resp;
|
|
160
355
|
try {
|
|
@@ -168,7 +363,7 @@ export function createOpenAIAdapter(config) {
|
|
|
168
363
|
Authorization: `Bearer ${config.apiKey}`,
|
|
169
364
|
},
|
|
170
365
|
body: JSON.stringify(body),
|
|
171
|
-
signal:
|
|
366
|
+
signal: combineRequestSignals(request.signal, request.timeout ?? defaultTimeout),
|
|
172
367
|
}),
|
|
173
368
|
});
|
|
174
369
|
}
|
|
@@ -206,6 +401,41 @@ export function createOpenAIAdapter(config) {
|
|
|
206
401
|
usage: { totalTokens: data.usage.total_tokens },
|
|
207
402
|
};
|
|
208
403
|
},
|
|
404
|
+
async listModels(filter) {
|
|
405
|
+
const isOfficial = baseUrl === 'https://api.openai.com/v1';
|
|
406
|
+
try {
|
|
407
|
+
const resp = await fetch(`${baseUrl}/models`, {
|
|
408
|
+
method: 'GET',
|
|
409
|
+
headers: { Authorization: `Bearer ${config.apiKey}` },
|
|
410
|
+
signal: AbortSignal.timeout(defaultTimeout),
|
|
411
|
+
});
|
|
412
|
+
if (!resp.ok) {
|
|
413
|
+
console.warn(`[plumbus] OpenAI listModels: ${baseUrl}/models returned ${resp.status} ${resp.statusText}; returning empty list`);
|
|
414
|
+
return [];
|
|
415
|
+
}
|
|
416
|
+
const data = (await resp.json());
|
|
417
|
+
if (!Array.isArray(data.data)) {
|
|
418
|
+
console.warn(`[plumbus] OpenAI listModels: ${baseUrl}/models returned unexpected response shape; returning empty list`);
|
|
419
|
+
return [];
|
|
420
|
+
}
|
|
421
|
+
const entries = data.data.map((m) => {
|
|
422
|
+
const e = { id: m.id };
|
|
423
|
+
if (typeof m.created === 'number') {
|
|
424
|
+
e.createdAt = new Date(m.created * 1000).toISOString();
|
|
425
|
+
}
|
|
426
|
+
if (typeof m.owned_by === 'string') {
|
|
427
|
+
e.ownedBy = m.owned_by;
|
|
428
|
+
}
|
|
429
|
+
return e;
|
|
430
|
+
});
|
|
431
|
+
return joinAndFilterModels({ provider: 'openai', entries, filter, isOfficial });
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
435
|
+
console.warn(`[plumbus] OpenAI listModels: ${baseUrl}/models failed (${msg}); returning empty list`);
|
|
436
|
+
return [];
|
|
437
|
+
}
|
|
438
|
+
},
|
|
209
439
|
};
|
|
210
440
|
}
|
|
211
441
|
export function createAnthropicAdapter(config) {
|
|
@@ -215,14 +445,27 @@ export function createAnthropicAdapter(config) {
|
|
|
215
445
|
return {
|
|
216
446
|
name: 'anthropic',
|
|
217
447
|
async complete(request) {
|
|
448
|
+
// Multi-turn path: when `messages` is supplied, send them verbatim.
|
|
449
|
+
// Single-turn path: synthesize a single user message from `prompt`.
|
|
450
|
+
const messages = request.messages && request.messages.length > 0
|
|
451
|
+
? request.messages.map((m) => ({ role: m.role, content: m.content }))
|
|
452
|
+
: [{ role: 'user', content: request.prompt }];
|
|
218
453
|
const body = {
|
|
219
454
|
model: request.model ?? defaultModel,
|
|
220
|
-
messages
|
|
455
|
+
messages,
|
|
221
456
|
max_tokens: request.maxTokens ?? 4096,
|
|
222
457
|
temperature: request.temperature ?? 0.7,
|
|
223
458
|
};
|
|
224
459
|
if (request.system)
|
|
225
460
|
body.system = request.system;
|
|
461
|
+
if (request.responseFormat === 'json' && request.responseSchema) {
|
|
462
|
+
body.output_config = {
|
|
463
|
+
format: {
|
|
464
|
+
type: 'json_schema',
|
|
465
|
+
schema: request.responseSchema,
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
}
|
|
226
469
|
const resp = await executeRequestWithRetry({
|
|
227
470
|
providerName: 'anthropic',
|
|
228
471
|
errorLabel: 'Anthropic API',
|
|
@@ -234,37 +477,66 @@ export function createAnthropicAdapter(config) {
|
|
|
234
477
|
'anthropic-version': '2023-06-01',
|
|
235
478
|
},
|
|
236
479
|
body: JSON.stringify(body),
|
|
237
|
-
signal:
|
|
480
|
+
signal: combineRequestSignals(request.signal, request.timeout ?? defaultTimeout),
|
|
238
481
|
}),
|
|
239
482
|
});
|
|
240
483
|
const data = (await resp.json());
|
|
484
|
+
const usage = anthropicUsageToTokenUsage(data.usage);
|
|
241
485
|
const text = data.content
|
|
242
486
|
.filter((c) => c.type === 'text')
|
|
243
487
|
.map((c) => c.text)
|
|
244
488
|
.join('');
|
|
489
|
+
if (data.stop_reason === 'refusal') {
|
|
490
|
+
throw new AIRefusalError({
|
|
491
|
+
provider: 'anthropic',
|
|
492
|
+
model: data.model,
|
|
493
|
+
refusalText: text || 'Request refused by Anthropic',
|
|
494
|
+
usage,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
// Mirror the OpenAI adapter: only throw on max_tokens when the caller
|
|
498
|
+
// requested structured JSON output. Free-text completions should return
|
|
499
|
+
// the partial content.
|
|
500
|
+
if (data.stop_reason === 'max_tokens' &&
|
|
501
|
+
(request.responseFormat === 'json' || !!request.responseSchema)) {
|
|
502
|
+
throw new AIIncompleteOutputError({
|
|
503
|
+
provider: 'anthropic',
|
|
504
|
+
model: data.model,
|
|
505
|
+
partialText: text,
|
|
506
|
+
usage,
|
|
507
|
+
finishReason: data.stop_reason,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
245
510
|
return {
|
|
246
511
|
content: text,
|
|
247
512
|
model: data.model,
|
|
248
|
-
usage
|
|
249
|
-
inputTokens: data.usage.input_tokens,
|
|
250
|
-
outputTokens: data.usage.output_tokens,
|
|
251
|
-
totalTokens: data.usage.input_tokens + data.usage.output_tokens,
|
|
252
|
-
cachedInputTokens: data.usage.cache_read_input_tokens ?? 0,
|
|
253
|
-
cacheWriteTokens: data.usage.cache_creation_input_tokens ?? 0,
|
|
254
|
-
},
|
|
513
|
+
usage,
|
|
255
514
|
finishReason: data.stop_reason,
|
|
256
515
|
};
|
|
257
516
|
},
|
|
258
517
|
async *stream(request) {
|
|
518
|
+
// Multi-turn path: when `messages` is supplied, send them verbatim.
|
|
519
|
+
// Single-turn path: synthesize a single user message from `prompt`.
|
|
520
|
+
const messages = request.messages && request.messages.length > 0
|
|
521
|
+
? request.messages.map((m) => ({ role: m.role, content: m.content }))
|
|
522
|
+
: [{ role: 'user', content: request.prompt }];
|
|
259
523
|
const body = {
|
|
260
524
|
model: request.model ?? defaultModel,
|
|
261
|
-
messages
|
|
525
|
+
messages,
|
|
262
526
|
max_tokens: request.maxTokens ?? 4096,
|
|
263
527
|
temperature: request.temperature ?? 0.7,
|
|
264
528
|
stream: true,
|
|
265
529
|
};
|
|
266
530
|
if (request.system)
|
|
267
531
|
body.system = request.system;
|
|
532
|
+
if (request.responseFormat === 'json' && request.responseSchema) {
|
|
533
|
+
body.output_config = {
|
|
534
|
+
format: {
|
|
535
|
+
type: 'json_schema',
|
|
536
|
+
schema: request.responseSchema,
|
|
537
|
+
},
|
|
538
|
+
};
|
|
539
|
+
}
|
|
268
540
|
let resp;
|
|
269
541
|
try {
|
|
270
542
|
resp = await executeRequestWithRetry({
|
|
@@ -278,7 +550,7 @@ export function createAnthropicAdapter(config) {
|
|
|
278
550
|
'anthropic-version': '2023-06-01',
|
|
279
551
|
},
|
|
280
552
|
body: JSON.stringify(body),
|
|
281
|
-
signal:
|
|
553
|
+
signal: combineRequestSignals(request.signal, request.timeout ?? defaultTimeout),
|
|
282
554
|
}),
|
|
283
555
|
});
|
|
284
556
|
}
|
|
@@ -296,6 +568,60 @@ export function createAnthropicAdapter(config) {
|
|
|
296
568
|
// Users should pair with an embedding-capable provider for RAG
|
|
297
569
|
throw new Error('Anthropic does not provide an embedding API. Use an OpenAI-compatible provider for embeddings.');
|
|
298
570
|
},
|
|
571
|
+
async listModels(filter) {
|
|
572
|
+
const isOfficial = baseUrl === 'https://api.anthropic.com/v1';
|
|
573
|
+
const entries = [];
|
|
574
|
+
let afterId;
|
|
575
|
+
try {
|
|
576
|
+
// Anthropic /models is paginated via `has_more` / `last_id` + `after_id` query param.
|
|
577
|
+
// Cap iterations defensively so a misbehaving API doesn't loop forever.
|
|
578
|
+
for (let page = 0; page < 20; page++) {
|
|
579
|
+
const url = new URL(`${baseUrl}/models`);
|
|
580
|
+
url.searchParams.set('limit', '100');
|
|
581
|
+
if (afterId)
|
|
582
|
+
url.searchParams.set('after_id', afterId);
|
|
583
|
+
const resp = await fetch(url, {
|
|
584
|
+
method: 'GET',
|
|
585
|
+
headers: {
|
|
586
|
+
'x-api-key': config.apiKey,
|
|
587
|
+
'anthropic-version': '2023-06-01',
|
|
588
|
+
},
|
|
589
|
+
signal: AbortSignal.timeout(defaultTimeout),
|
|
590
|
+
});
|
|
591
|
+
if (!resp.ok) {
|
|
592
|
+
console.warn(`[plumbus] Anthropic listModels: ${url} returned ${resp.status} ${resp.statusText}; returning ${entries.length === 0 ? 'empty' : 'partial'} list`);
|
|
593
|
+
if (entries.length === 0)
|
|
594
|
+
return [];
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
const data = (await resp.json());
|
|
598
|
+
if (!Array.isArray(data.data)) {
|
|
599
|
+
console.warn(`[plumbus] Anthropic listModels: ${url} returned unexpected response shape; returning ${entries.length === 0 ? 'empty' : 'partial'} list`);
|
|
600
|
+
if (entries.length === 0)
|
|
601
|
+
return [];
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
for (const m of data.data) {
|
|
605
|
+
const e = { id: m.id };
|
|
606
|
+
if (typeof m.display_name === 'string')
|
|
607
|
+
e.displayName = m.display_name;
|
|
608
|
+
if (typeof m.created_at === 'string')
|
|
609
|
+
e.createdAt = m.created_at;
|
|
610
|
+
entries.push(e);
|
|
611
|
+
}
|
|
612
|
+
if (!data.has_more || !data.last_id)
|
|
613
|
+
break;
|
|
614
|
+
afterId = data.last_id;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
catch (err) {
|
|
618
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
619
|
+
console.warn(`[plumbus] Anthropic listModels: ${baseUrl}/models failed (${msg}); returning ${entries.length === 0 ? 'empty' : 'partial'} list`);
|
|
620
|
+
if (entries.length === 0)
|
|
621
|
+
return [];
|
|
622
|
+
}
|
|
623
|
+
return joinAndFilterModels({ provider: 'anthropic', entries, filter, isOfficial });
|
|
624
|
+
},
|
|
299
625
|
};
|
|
300
626
|
}
|
|
301
627
|
async function* parseSSEStream(resp, parseChunk) {
|