@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.
Files changed (182) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/ai/__tests__/ai-service.test.js +704 -1
  3. package/dist/ai/__tests__/ai-service.test.js.map +1 -1
  4. package/dist/ai/__tests__/chunking.test.js +34 -0
  5. package/dist/ai/__tests__/chunking.test.js.map +1 -1
  6. package/dist/ai/__tests__/model-pricing.test.js +45 -1
  7. package/dist/ai/__tests__/model-pricing.test.js.map +1 -1
  8. package/dist/ai/__tests__/provider.test.d.ts.map +1 -1
  9. package/dist/ai/__tests__/provider.test.js +730 -1
  10. package/dist/ai/__tests__/provider.test.js.map +1 -1
  11. package/dist/ai/__tests__/validation.test.js +200 -1
  12. package/dist/ai/__tests__/validation.test.js.map +1 -1
  13. package/dist/ai/__tests__/zod-to-provider-schema.test.d.ts +2 -0
  14. package/dist/ai/__tests__/zod-to-provider-schema.test.d.ts.map +1 -0
  15. package/dist/ai/__tests__/zod-to-provider-schema.test.js +60 -0
  16. package/dist/ai/__tests__/zod-to-provider-schema.test.js.map +1 -0
  17. package/dist/ai/ai-service.d.ts +19 -2
  18. package/dist/ai/ai-service.d.ts.map +1 -1
  19. package/dist/ai/ai-service.js +544 -110
  20. package/dist/ai/ai-service.js.map +1 -1
  21. package/dist/ai/cost-tracker.d.ts +26 -1
  22. package/dist/ai/cost-tracker.d.ts.map +1 -1
  23. package/dist/ai/cost-tracker.js +1 -0
  24. package/dist/ai/cost-tracker.js.map +1 -1
  25. package/dist/ai/index.d.ts +6 -4
  26. package/dist/ai/index.d.ts.map +1 -1
  27. package/dist/ai/index.js +7 -3
  28. package/dist/ai/index.js.map +1 -1
  29. package/dist/ai/model-pricing.d.ts +24 -1
  30. package/dist/ai/model-pricing.d.ts.map +1 -1
  31. package/dist/ai/model-pricing.js +83 -56
  32. package/dist/ai/model-pricing.js.map +1 -1
  33. package/dist/ai/provider.d.ts +110 -1
  34. package/dist/ai/provider.d.ts.map +1 -1
  35. package/dist/ai/provider.js +358 -32
  36. package/dist/ai/provider.js.map +1 -1
  37. package/dist/ai/rag/chunking.js +53 -3
  38. package/dist/ai/rag/chunking.js.map +1 -1
  39. package/dist/ai/refusal.d.ts +28 -0
  40. package/dist/ai/refusal.d.ts.map +1 -0
  41. package/dist/ai/refusal.js +36 -0
  42. package/dist/ai/refusal.js.map +1 -0
  43. package/dist/ai/validation.d.ts +40 -8
  44. package/dist/ai/validation.d.ts.map +1 -1
  45. package/dist/ai/validation.js +97 -6
  46. package/dist/ai/validation.js.map +1 -1
  47. package/dist/ai/zod-to-provider-schema.d.ts +15 -0
  48. package/dist/ai/zod-to-provider-schema.d.ts.map +1 -0
  49. package/dist/ai/zod-to-provider-schema.js +200 -0
  50. package/dist/ai/zod-to-provider-schema.js.map +1 -0
  51. package/dist/cli/__tests__/doctor.test.js +104 -2
  52. package/dist/cli/__tests__/doctor.test.js.map +1 -1
  53. package/dist/cli/__tests__/init.test.js +127 -7
  54. package/dist/cli/__tests__/init.test.js.map +1 -1
  55. package/dist/cli/__tests__/migrate.test.js +159 -2
  56. package/dist/cli/__tests__/migrate.test.js.map +1 -1
  57. package/dist/cli/commands/__tests__/commands.test.js +5 -0
  58. package/dist/cli/commands/__tests__/commands.test.js.map +1 -1
  59. package/dist/cli/commands/dev.d.ts.map +1 -1
  60. package/dist/cli/commands/dev.js +21 -1
  61. package/dist/cli/commands/dev.js.map +1 -1
  62. package/dist/cli/commands/doctor.d.ts +2 -0
  63. package/dist/cli/commands/doctor.d.ts.map +1 -1
  64. package/dist/cli/commands/doctor.js +78 -0
  65. package/dist/cli/commands/doctor.js.map +1 -1
  66. package/dist/cli/commands/generate.d.ts.map +1 -1
  67. package/dist/cli/commands/generate.js +7 -2
  68. package/dist/cli/commands/generate.js.map +1 -1
  69. package/dist/cli/commands/init.d.ts +16 -2
  70. package/dist/cli/commands/init.d.ts.map +1 -1
  71. package/dist/cli/commands/init.js +226 -25
  72. package/dist/cli/commands/init.js.map +1 -1
  73. package/dist/cli/commands/migrate.d.ts.map +1 -1
  74. package/dist/cli/commands/migrate.js +258 -8
  75. package/dist/cli/commands/migrate.js.map +1 -1
  76. package/dist/cli/commands/start.d.ts.map +1 -1
  77. package/dist/cli/commands/start.js +21 -1
  78. package/dist/cli/commands/start.js.map +1 -1
  79. package/dist/data/__tests__/drift-inspector.test.d.ts +2 -0
  80. package/dist/data/__tests__/drift-inspector.test.d.ts.map +1 -0
  81. package/dist/data/__tests__/drift-inspector.test.js +170 -0
  82. package/dist/data/__tests__/drift-inspector.test.js.map +1 -0
  83. package/dist/data/__tests__/framework-migration-rewriter.test.d.ts +2 -0
  84. package/dist/data/__tests__/framework-migration-rewriter.test.d.ts.map +1 -0
  85. package/dist/data/__tests__/framework-migration-rewriter.test.js +329 -0
  86. package/dist/data/__tests__/framework-migration-rewriter.test.js.map +1 -0
  87. package/dist/data/__tests__/migration.test.js +93 -2
  88. package/dist/data/__tests__/migration.test.js.map +1 -1
  89. package/dist/data/__tests__/repository.test.js +126 -0
  90. package/dist/data/__tests__/repository.test.js.map +1 -1
  91. package/dist/data/drift-inspector.d.ts +41 -0
  92. package/dist/data/drift-inspector.d.ts.map +1 -0
  93. package/dist/data/drift-inspector.js +197 -0
  94. package/dist/data/drift-inspector.js.map +1 -0
  95. package/dist/data/framework-migration-rewriter.d.ts +61 -0
  96. package/dist/data/framework-migration-rewriter.d.ts.map +1 -0
  97. package/dist/data/framework-migration-rewriter.js +357 -0
  98. package/dist/data/framework-migration-rewriter.js.map +1 -0
  99. package/dist/data/index.d.ts +2 -1
  100. package/dist/data/index.d.ts.map +1 -1
  101. package/dist/data/index.js +2 -1
  102. package/dist/data/index.js.map +1 -1
  103. package/dist/data/migration.d.ts +31 -1
  104. package/dist/data/migration.d.ts.map +1 -1
  105. package/dist/data/migration.js +113 -3
  106. package/dist/data/migration.js.map +1 -1
  107. package/dist/data/repository.d.ts.map +1 -1
  108. package/dist/data/repository.js +21 -0
  109. package/dist/data/repository.js.map +1 -1
  110. package/dist/define/__tests__/definePrompt.test.js +37 -0
  111. package/dist/define/__tests__/definePrompt.test.js.map +1 -1
  112. package/dist/define/definePrompt.d.ts +13 -0
  113. package/dist/define/definePrompt.d.ts.map +1 -1
  114. package/dist/define/definePrompt.js +3 -0
  115. package/dist/define/definePrompt.js.map +1 -1
  116. package/dist/errors/http.d.ts.map +1 -1
  117. package/dist/errors/http.js +2 -0
  118. package/dist/errors/http.js.map +1 -1
  119. package/dist/errors/index.d.ts +12 -0
  120. package/dist/errors/index.d.ts.map +1 -1
  121. package/dist/errors/index.js +21 -0
  122. package/dist/errors/index.js.map +1 -1
  123. package/dist/events/__tests__/event-emitter.test.js +72 -0
  124. package/dist/events/__tests__/event-emitter.test.js.map +1 -1
  125. package/dist/events/emitter.d.ts.map +1 -1
  126. package/dist/events/emitter.js +51 -0
  127. package/dist/events/emitter.js.map +1 -1
  128. package/dist/execution/__tests__/context-factory.test.js +1 -0
  129. package/dist/execution/__tests__/context-factory.test.js.map +1 -1
  130. package/dist/execution/context-factory.d.ts.map +1 -1
  131. package/dist/execution/context-factory.js +2 -0
  132. package/dist/execution/context-factory.js.map +1 -1
  133. package/dist/flows/__tests__/flow-engine.test.js +451 -4
  134. package/dist/flows/__tests__/flow-engine.test.js.map +1 -1
  135. package/dist/flows/__tests__/flow-service.test.js +8 -0
  136. package/dist/flows/__tests__/flow-service.test.js.map +1 -1
  137. package/dist/flows/__tests__/step-executor.test.js +8 -2
  138. package/dist/flows/__tests__/step-executor.test.js.map +1 -1
  139. package/dist/flows/engine.d.ts +50 -1
  140. package/dist/flows/engine.d.ts.map +1 -1
  141. package/dist/flows/engine.js +427 -23
  142. package/dist/flows/engine.js.map +1 -1
  143. package/dist/flows/flow-service.d.ts.map +1 -1
  144. package/dist/flows/flow-service.js +4 -0
  145. package/dist/flows/flow-service.js.map +1 -1
  146. package/dist/flows/index.d.ts +1 -1
  147. package/dist/flows/index.d.ts.map +1 -1
  148. package/dist/flows/index.js +1 -1
  149. package/dist/flows/index.js.map +1 -1
  150. package/dist/flows/schema.d.ts +34 -0
  151. package/dist/flows/schema.d.ts.map +1 -1
  152. package/dist/flows/schema.js +3 -0
  153. package/dist/flows/schema.js.map +1 -1
  154. package/dist/index.d.ts +4 -4
  155. package/dist/index.d.ts.map +1 -1
  156. package/dist/index.js +3 -3
  157. package/dist/index.js.map +1 -1
  158. package/dist/server/bootstrap.d.ts +14 -0
  159. package/dist/server/bootstrap.d.ts.map +1 -1
  160. package/dist/server/bootstrap.js +23 -4
  161. package/dist/server/bootstrap.js.map +1 -1
  162. package/dist/testing/context.d.ts.map +1 -1
  163. package/dist/testing/context.js +37 -1
  164. package/dist/testing/context.js.map +1 -1
  165. package/dist/types/context.d.ts +110 -0
  166. package/dist/types/context.d.ts.map +1 -1
  167. package/dist/types/enums.d.ts +2 -0
  168. package/dist/types/enums.d.ts.map +1 -1
  169. package/dist/types/enums.js +2 -0
  170. package/dist/types/enums.js.map +1 -1
  171. package/dist/types/prompt.d.ts +64 -0
  172. package/dist/types/prompt.d.ts.map +1 -1
  173. package/dist/worker/__tests__/bootstrap.test.js +36 -2
  174. package/dist/worker/__tests__/bootstrap.test.js.map +1 -1
  175. package/dist/worker/bootstrap.d.ts +7 -0
  176. package/dist/worker/bootstrap.d.ts.map +1 -1
  177. package/dist/worker/bootstrap.js +153 -12
  178. package/dist/worker/bootstrap.js.map +1 -1
  179. package/instructions/framework.md +7 -0
  180. package/instructions/guardrails.md +39 -0
  181. package/instructions/patterns.md +10 -0
  182. package/package.json +3 -2
@@ -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
- /** User prompt content */
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":"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"}
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/ai/provider.ts"],"names":[],"mappings":"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"}
@@ -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
- messages.push({ role: 'user', content: request.prompt });
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: request.model ?? defaultModel,
248
+ model,
104
249
  messages,
105
250
  temperature: request.temperature ?? 0.7,
106
251
  };
107
- if (request.maxTokens)
108
- body.max_tokens = request.maxTokens;
109
- if (request.responseFormat === 'json') {
110
- body.response_format = { type: 'json_object' };
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: AbortSignal.timeout(request.timeout ?? defaultTimeout),
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: choice.message.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
- messages.push({ role: 'user', content: request.prompt });
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: request.model ?? defaultModel,
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
- if (request.maxTokens)
155
- body.max_tokens = request.maxTokens;
156
- if (request.responseFormat === 'json') {
157
- body.response_format = { type: 'json_object' };
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: AbortSignal.timeout(request.timeout ?? defaultTimeout),
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: [{ role: 'user', content: request.prompt }],
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: AbortSignal.timeout(request.timeout ?? defaultTimeout),
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: [{ role: 'user', content: request.prompt }],
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: AbortSignal.timeout(request.timeout ?? defaultTimeout),
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) {