@animalabs/membrane 0.1.19 → 0.2.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.
@@ -5,4 +5,5 @@ export { AnthropicAdapter, toAnthropicContent, fromAnthropicContent, type Anthro
5
5
  export { OpenRouterAdapter, toOpenRouterMessages, fromOpenRouterMessage, type OpenRouterAdapterConfig, } from './openrouter.js';
6
6
  export { OpenAIAdapter, toOpenAIContent, fromOpenAIContent, type OpenAIAdapterConfig, } from './openai.js';
7
7
  export { OpenAICompatibleAdapter, toOpenAIMessages, fromOpenAIMessage, type OpenAICompatibleAdapterConfig, } from './openai-compatible.js';
8
+ export { OpenAICompletionsAdapter, type OpenAICompletionsAdapterConfig, } from './openai-completions.js';
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,KAAK,sBAAsB,GAC5B,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,EACrB,KAAK,uBAAuB,GAC7B,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,KAAK,mBAAmB,GACzB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,6BAA6B,GACnC,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,KAAK,sBAAsB,GAC5B,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,EACrB,KAAK,uBAAuB,GAC7B,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,KAAK,mBAAmB,GACzB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,6BAA6B,GACnC,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,wBAAwB,EACxB,KAAK,8BAA8B,GACpC,MAAM,yBAAyB,CAAC"}
@@ -5,4 +5,5 @@ export { AnthropicAdapter, toAnthropicContent, fromAnthropicContent, } from './a
5
5
  export { OpenRouterAdapter, toOpenRouterMessages, fromOpenRouterMessage, } from './openrouter.js';
6
6
  export { OpenAIAdapter, toOpenAIContent, fromOpenAIContent, } from './openai.js';
7
7
  export { OpenAICompatibleAdapter, toOpenAIMessages, fromOpenAIMessage, } from './openai-compatible.js';
8
+ export { OpenAICompletionsAdapter, } from './openai-completions.js';
8
9
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,GAErB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,GAEtB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,aAAa,EACb,eAAe,EACf,iBAAiB,GAElB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,GAElB,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,GAErB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,GAEtB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,aAAa,EACb,eAAe,EACf,iBAAiB,GAElB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,GAElB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,wBAAwB,GAEzB,MAAM,yBAAyB,CAAC"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * OpenAI-Compatible Completions adapter for base models
3
+ *
4
+ * For true base/completion models that use the `/v1/completions` endpoint:
5
+ * - No chat formatting built-in
6
+ * - Single text prompt input
7
+ * - Raw completion output
8
+ * - No image support
9
+ *
10
+ * Serializes conversations to Human:/Assistant: format.
11
+ */
12
+ import type { ProviderAdapter, ProviderRequest, ProviderRequestOptions, ProviderResponse, StreamCallbacks } from '../types/index.js';
13
+ export interface OpenAICompletionsAdapterConfig {
14
+ /** Base URL for the API (required, e.g., 'http://localhost:8000/v1') */
15
+ baseURL: string;
16
+ /** API key (optional for local servers) */
17
+ apiKey?: string;
18
+ /** Provider name for logging/identification (default: 'openai-completions') */
19
+ providerName?: string;
20
+ /** Default max tokens */
21
+ defaultMaxTokens?: number;
22
+ /** Additional headers to include with requests */
23
+ extraHeaders?: Record<string, string>;
24
+ /**
25
+ * Stop sequences to use (default: ['\n\nHuman:', '\nHuman:'])
26
+ * These prevent the model from generating user turns.
27
+ */
28
+ defaultStopSequences?: string[];
29
+ /**
30
+ * Whether to warn when images are stripped from context (default: true)
31
+ */
32
+ warnOnImageStrip?: boolean;
33
+ }
34
+ export declare class OpenAICompletionsAdapter implements ProviderAdapter {
35
+ readonly name: string;
36
+ private baseURL;
37
+ private apiKey;
38
+ private defaultMaxTokens;
39
+ private extraHeaders;
40
+ private defaultStopSequences;
41
+ private warnOnImageStrip;
42
+ constructor(config: OpenAICompletionsAdapterConfig);
43
+ supportsModel(_modelId: string): boolean;
44
+ complete(request: ProviderRequest, options?: ProviderRequestOptions): Promise<ProviderResponse>;
45
+ stream(request: ProviderRequest, callbacks: StreamCallbacks, options?: ProviderRequestOptions): Promise<ProviderResponse>;
46
+ /**
47
+ * Serialize messages to Human:/Assistant: format for base models.
48
+ * Images are stripped from content.
49
+ */
50
+ serializeToPrompt(messages: any[]): string;
51
+ private normalizeRole;
52
+ private extractTextContent;
53
+ private getHeaders;
54
+ private buildRequest;
55
+ private makeRequest;
56
+ private parseResponse;
57
+ private buildStreamedResponse;
58
+ private textToContent;
59
+ private mapFinishReason;
60
+ private handleError;
61
+ }
62
+ //# sourceMappingURL=openai-completions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai-completions.d.ts","sourceRoot":"","sources":["../../src/providers/openai-completions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,sBAAsB,EACtB,gBAAgB,EAChB,eAAe,EAEhB,MAAM,mBAAmB,CAAC;AA2C3B,MAAM,WAAW,8BAA8B;IAC7C,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC;IAEhB,2CAA2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,yBAAyB;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEtC;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAEhC;;OAEG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAMD,qBAAa,wBAAyB,YAAW,eAAe;IAC9D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,YAAY,CAAyB;IAC7C,OAAO,CAAC,oBAAoB,CAAW;IACvC,OAAO,CAAC,gBAAgB,CAAU;gBAEtB,MAAM,EAAE,8BAA8B;IAclD,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAIlC,QAAQ,CACZ,OAAO,EAAE,eAAe,EACxB,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,gBAAgB,CAAC;IAWtB,MAAM,CACV,OAAO,EAAE,eAAe,EACxB,SAAS,EAAE,eAAe,EAC1B,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,gBAAgB,CAAC;IAkE5B;;;OAGG;IACH,iBAAiB,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAG,MAAM;IA6B1C,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,kBAAkB;IA4B1B,OAAO,CAAC,UAAU;IAalB,OAAO,CAAC,YAAY;YA+BN,WAAW;IAgBzB,OAAO,CAAC,aAAa;IAkBrB,OAAO,CAAC,qBAAqB;IAoB7B,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,WAAW;CAqCpB"}
@@ -0,0 +1,286 @@
1
+ /**
2
+ * OpenAI-Compatible Completions adapter for base models
3
+ *
4
+ * For true base/completion models that use the `/v1/completions` endpoint:
5
+ * - No chat formatting built-in
6
+ * - Single text prompt input
7
+ * - Raw completion output
8
+ * - No image support
9
+ *
10
+ * Serializes conversations to Human:/Assistant: format.
11
+ */
12
+ import { MembraneError, rateLimitError, contextLengthError, authError, serverError, abortError, networkError, } from '../types/index.js';
13
+ // ============================================================================
14
+ // OpenAI Completions Adapter
15
+ // ============================================================================
16
+ export class OpenAICompletionsAdapter {
17
+ name;
18
+ baseURL;
19
+ apiKey;
20
+ defaultMaxTokens;
21
+ extraHeaders;
22
+ defaultStopSequences;
23
+ warnOnImageStrip;
24
+ constructor(config) {
25
+ if (!config.baseURL) {
26
+ throw new Error('OpenAI completions adapter requires baseURL');
27
+ }
28
+ this.name = config.providerName ?? 'openai-completions';
29
+ this.baseURL = config.baseURL.replace(/\/$/, ''); // Remove trailing slash
30
+ this.apiKey = config.apiKey ?? '';
31
+ this.defaultMaxTokens = config.defaultMaxTokens ?? 4096;
32
+ this.extraHeaders = config.extraHeaders ?? {};
33
+ this.defaultStopSequences = config.defaultStopSequences ?? ['\n\nHuman:', '\nHuman:'];
34
+ this.warnOnImageStrip = config.warnOnImageStrip ?? true;
35
+ }
36
+ supportsModel(_modelId) {
37
+ return true;
38
+ }
39
+ async complete(request, options) {
40
+ const completionsRequest = this.buildRequest(request);
41
+ try {
42
+ const response = await this.makeRequest(completionsRequest, options);
43
+ return this.parseResponse(response, request.model, completionsRequest);
44
+ }
45
+ catch (error) {
46
+ throw this.handleError(error, completionsRequest);
47
+ }
48
+ }
49
+ async stream(request, callbacks, options) {
50
+ const completionsRequest = this.buildRequest(request);
51
+ completionsRequest.stream = true;
52
+ try {
53
+ const response = await fetch(`${this.baseURL}/completions`, {
54
+ method: 'POST',
55
+ headers: this.getHeaders(),
56
+ body: JSON.stringify(completionsRequest),
57
+ signal: options?.signal,
58
+ });
59
+ if (!response.ok) {
60
+ const errorText = await response.text();
61
+ throw new Error(`API error: ${response.status} ${errorText}`);
62
+ }
63
+ const reader = response.body?.getReader();
64
+ if (!reader) {
65
+ throw new Error('No response body');
66
+ }
67
+ const decoder = new TextDecoder();
68
+ let accumulated = '';
69
+ let finishReason = 'stop';
70
+ while (true) {
71
+ const { done, value } = await reader.read();
72
+ if (done)
73
+ break;
74
+ const chunk = decoder.decode(value, { stream: true });
75
+ const lines = chunk.split('\n').filter(line => line.startsWith('data: '));
76
+ for (const line of lines) {
77
+ const data = line.slice(6);
78
+ if (data === '[DONE]')
79
+ continue;
80
+ try {
81
+ const parsed = JSON.parse(data);
82
+ const text = parsed.choices?.[0]?.text;
83
+ if (text) {
84
+ accumulated += text;
85
+ callbacks.onChunk(text);
86
+ }
87
+ if (parsed.choices?.[0]?.finish_reason) {
88
+ finishReason = parsed.choices[0].finish_reason;
89
+ }
90
+ }
91
+ catch {
92
+ // Ignore parse errors in stream
93
+ }
94
+ }
95
+ }
96
+ return this.buildStreamedResponse(accumulated, finishReason, request.model, completionsRequest);
97
+ }
98
+ catch (error) {
99
+ throw this.handleError(error, completionsRequest);
100
+ }
101
+ }
102
+ // ============================================================================
103
+ // Prompt Serialization
104
+ // ============================================================================
105
+ /**
106
+ * Serialize messages to Human:/Assistant: format for base models.
107
+ * Images are stripped from content.
108
+ */
109
+ serializeToPrompt(messages) {
110
+ const parts = [];
111
+ let hasStrippedImages = false;
112
+ for (const msg of messages) {
113
+ const role = this.normalizeRole(msg.role);
114
+ const prefix = role === 'user' ? 'Human:' : 'Assistant:';
115
+ // Extract text content, strip images
116
+ const textContent = this.extractTextContent(msg.content);
117
+ if (textContent.hadImages) {
118
+ hasStrippedImages = true;
119
+ }
120
+ if (textContent.text) {
121
+ parts.push(`${prefix} ${textContent.text}`);
122
+ }
123
+ }
124
+ if (hasStrippedImages && this.warnOnImageStrip) {
125
+ console.warn('[OpenAICompletionsAdapter] Images were stripped from context (not supported in completions mode)');
126
+ }
127
+ // Add final Assistant: prefix to prompt completion
128
+ parts.push('Assistant:');
129
+ return parts.join('\n\n');
130
+ }
131
+ normalizeRole(role) {
132
+ if (role === 'user' || role === 'human' || role === 'Human') {
133
+ return 'user';
134
+ }
135
+ return 'assistant';
136
+ }
137
+ extractTextContent(content) {
138
+ if (typeof content === 'string') {
139
+ return { text: content, hadImages: false };
140
+ }
141
+ if (Array.isArray(content)) {
142
+ const textParts = [];
143
+ let hadImages = false;
144
+ for (const block of content) {
145
+ if (block.type === 'text') {
146
+ textParts.push(block.text);
147
+ }
148
+ else if (block.type === 'image' || block.type === 'image_url') {
149
+ hadImages = true;
150
+ }
151
+ // Skip tool_use, tool_result, thinking blocks for base models
152
+ }
153
+ return { text: textParts.join('\n'), hadImages };
154
+ }
155
+ return { text: '', hadImages: false };
156
+ }
157
+ // ============================================================================
158
+ // Private Methods
159
+ // ============================================================================
160
+ getHeaders() {
161
+ const headers = {
162
+ 'Content-Type': 'application/json',
163
+ ...this.extraHeaders,
164
+ };
165
+ if (this.apiKey) {
166
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
167
+ }
168
+ return headers;
169
+ }
170
+ buildRequest(request) {
171
+ const prompt = this.serializeToPrompt(request.messages);
172
+ const params = {
173
+ model: request.model,
174
+ prompt,
175
+ max_tokens: request.maxTokens || this.defaultMaxTokens,
176
+ };
177
+ if (request.temperature !== undefined) {
178
+ params.temperature = request.temperature;
179
+ }
180
+ // Combine default stop sequences with any provided ones
181
+ const stopSequences = [
182
+ ...this.defaultStopSequences,
183
+ ...(request.stopSequences || []),
184
+ ];
185
+ if (stopSequences.length > 0) {
186
+ params.stop = stopSequences;
187
+ }
188
+ // Apply extra params (but not messages/tools which don't apply)
189
+ if (request.extra) {
190
+ const { messages, tools, ...rest } = request.extra;
191
+ Object.assign(params, rest);
192
+ }
193
+ return params;
194
+ }
195
+ async makeRequest(request, options) {
196
+ const response = await fetch(`${this.baseURL}/completions`, {
197
+ method: 'POST',
198
+ headers: this.getHeaders(),
199
+ body: JSON.stringify(request),
200
+ signal: options?.signal,
201
+ });
202
+ if (!response.ok) {
203
+ const errorText = await response.text();
204
+ throw new Error(`API error: ${response.status} ${errorText}`);
205
+ }
206
+ return response.json();
207
+ }
208
+ parseResponse(response, requestedModel, rawRequest) {
209
+ const choice = response.choices[0];
210
+ const text = choice?.text ?? '';
211
+ return {
212
+ content: this.textToContent(text),
213
+ stopReason: this.mapFinishReason(choice?.finish_reason),
214
+ stopSequence: undefined,
215
+ usage: {
216
+ inputTokens: response.usage?.prompt_tokens ?? 0,
217
+ outputTokens: response.usage?.completion_tokens ?? 0,
218
+ },
219
+ model: response.model ?? requestedModel,
220
+ rawRequest,
221
+ raw: response,
222
+ };
223
+ }
224
+ buildStreamedResponse(accumulated, finishReason, requestedModel, rawRequest) {
225
+ return {
226
+ content: this.textToContent(accumulated),
227
+ stopReason: this.mapFinishReason(finishReason),
228
+ stopSequence: undefined,
229
+ usage: {
230
+ inputTokens: 0, // Not available in streaming
231
+ outputTokens: 0,
232
+ },
233
+ model: requestedModel,
234
+ rawRequest,
235
+ raw: { text: accumulated, finish_reason: finishReason },
236
+ };
237
+ }
238
+ textToContent(text) {
239
+ // Trim leading whitespace (model often starts with space after "Assistant:")
240
+ const trimmed = text.replace(/^\s+/, '');
241
+ if (!trimmed)
242
+ return [];
243
+ return [{ type: 'text', text: trimmed }];
244
+ }
245
+ mapFinishReason(reason) {
246
+ switch (reason) {
247
+ case 'stop':
248
+ return 'end_turn';
249
+ case 'length':
250
+ return 'max_tokens';
251
+ default:
252
+ return 'end_turn';
253
+ }
254
+ }
255
+ handleError(error, rawRequest) {
256
+ if (error instanceof Error) {
257
+ const message = error.message;
258
+ if (message.includes('429') || message.includes('rate')) {
259
+ return rateLimitError(message, undefined, error, rawRequest);
260
+ }
261
+ if (message.includes('401') || message.includes('auth') || message.includes('Unauthorized')) {
262
+ return authError(message, error, rawRequest);
263
+ }
264
+ if (message.includes('context') || message.includes('too long') || message.includes('maximum context')) {
265
+ return contextLengthError(message, error, rawRequest);
266
+ }
267
+ if (message.includes('500') || message.includes('502') || message.includes('503')) {
268
+ return serverError(message, undefined, error, rawRequest);
269
+ }
270
+ if (error.name === 'AbortError') {
271
+ return abortError(undefined, rawRequest);
272
+ }
273
+ if (message.includes('network') || message.includes('fetch') || message.includes('ECONNREFUSED')) {
274
+ return networkError(message, error, rawRequest);
275
+ }
276
+ }
277
+ return new MembraneError({
278
+ type: 'unknown',
279
+ message: error instanceof Error ? error.message : String(error),
280
+ retryable: false,
281
+ rawError: error,
282
+ rawRequest,
283
+ });
284
+ }
285
+ }
286
+ //# sourceMappingURL=openai-completions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai-completions.js","sourceRoot":"","sources":["../../src/providers/openai-completions.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@animalabs/membrane",
3
- "version": "0.1.19",
3
+ "version": "0.2.0",
4
4
  "description": "LLM middleware - a selective boundary that transforms what passes through",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -29,3 +29,8 @@ export {
29
29
  fromOpenAIMessage,
30
30
  type OpenAICompatibleAdapterConfig,
31
31
  } from './openai-compatible.js';
32
+
33
+ export {
34
+ OpenAICompletionsAdapter,
35
+ type OpenAICompletionsAdapterConfig,
36
+ } from './openai-completions.js';
@@ -0,0 +1,429 @@
1
+ /**
2
+ * OpenAI-Compatible Completions adapter for base models
3
+ *
4
+ * For true base/completion models that use the `/v1/completions` endpoint:
5
+ * - No chat formatting built-in
6
+ * - Single text prompt input
7
+ * - Raw completion output
8
+ * - No image support
9
+ *
10
+ * Serializes conversations to Human:/Assistant: format.
11
+ */
12
+
13
+ import type {
14
+ ProviderAdapter,
15
+ ProviderRequest,
16
+ ProviderRequestOptions,
17
+ ProviderResponse,
18
+ StreamCallbacks,
19
+ ContentBlock,
20
+ } from '../types/index.js';
21
+ import {
22
+ MembraneError,
23
+ rateLimitError,
24
+ contextLengthError,
25
+ authError,
26
+ serverError,
27
+ abortError,
28
+ networkError,
29
+ } from '../types/index.js';
30
+
31
+ // ============================================================================
32
+ // Types
33
+ // ============================================================================
34
+
35
+ interface CompletionsRequest {
36
+ model: string;
37
+ prompt: string;
38
+ max_tokens?: number;
39
+ temperature?: number;
40
+ stop?: string[];
41
+ stream?: boolean;
42
+ }
43
+
44
+ interface CompletionsResponse {
45
+ id: string;
46
+ model: string;
47
+ choices: {
48
+ index: number;
49
+ text: string;
50
+ finish_reason: string;
51
+ }[];
52
+ usage?: {
53
+ prompt_tokens: number;
54
+ completion_tokens: number;
55
+ total_tokens: number;
56
+ };
57
+ }
58
+
59
+ // ============================================================================
60
+ // Adapter Configuration
61
+ // ============================================================================
62
+
63
+ export interface OpenAICompletionsAdapterConfig {
64
+ /** Base URL for the API (required, e.g., 'http://localhost:8000/v1') */
65
+ baseURL: string;
66
+
67
+ /** API key (optional for local servers) */
68
+ apiKey?: string;
69
+
70
+ /** Provider name for logging/identification (default: 'openai-completions') */
71
+ providerName?: string;
72
+
73
+ /** Default max tokens */
74
+ defaultMaxTokens?: number;
75
+
76
+ /** Additional headers to include with requests */
77
+ extraHeaders?: Record<string, string>;
78
+
79
+ /**
80
+ * Stop sequences to use (default: ['\n\nHuman:', '\nHuman:'])
81
+ * These prevent the model from generating user turns.
82
+ */
83
+ defaultStopSequences?: string[];
84
+
85
+ /**
86
+ * Whether to warn when images are stripped from context (default: true)
87
+ */
88
+ warnOnImageStrip?: boolean;
89
+ }
90
+
91
+ // ============================================================================
92
+ // OpenAI Completions Adapter
93
+ // ============================================================================
94
+
95
+ export class OpenAICompletionsAdapter implements ProviderAdapter {
96
+ readonly name: string;
97
+ private baseURL: string;
98
+ private apiKey: string;
99
+ private defaultMaxTokens: number;
100
+ private extraHeaders: Record<string, string>;
101
+ private defaultStopSequences: string[];
102
+ private warnOnImageStrip: boolean;
103
+
104
+ constructor(config: OpenAICompletionsAdapterConfig) {
105
+ if (!config.baseURL) {
106
+ throw new Error('OpenAI completions adapter requires baseURL');
107
+ }
108
+
109
+ this.name = config.providerName ?? 'openai-completions';
110
+ this.baseURL = config.baseURL.replace(/\/$/, ''); // Remove trailing slash
111
+ this.apiKey = config.apiKey ?? '';
112
+ this.defaultMaxTokens = config.defaultMaxTokens ?? 4096;
113
+ this.extraHeaders = config.extraHeaders ?? {};
114
+ this.defaultStopSequences = config.defaultStopSequences ?? ['\n\nHuman:', '\nHuman:'];
115
+ this.warnOnImageStrip = config.warnOnImageStrip ?? true;
116
+ }
117
+
118
+ supportsModel(_modelId: string): boolean {
119
+ return true;
120
+ }
121
+
122
+ async complete(
123
+ request: ProviderRequest,
124
+ options?: ProviderRequestOptions
125
+ ): Promise<ProviderResponse> {
126
+ const completionsRequest = this.buildRequest(request);
127
+
128
+ try {
129
+ const response = await this.makeRequest(completionsRequest, options);
130
+ return this.parseResponse(response, request.model, completionsRequest);
131
+ } catch (error) {
132
+ throw this.handleError(error, completionsRequest);
133
+ }
134
+ }
135
+
136
+ async stream(
137
+ request: ProviderRequest,
138
+ callbacks: StreamCallbacks,
139
+ options?: ProviderRequestOptions
140
+ ): Promise<ProviderResponse> {
141
+ const completionsRequest = this.buildRequest(request);
142
+ completionsRequest.stream = true;
143
+
144
+ try {
145
+ const response = await fetch(`${this.baseURL}/completions`, {
146
+ method: 'POST',
147
+ headers: this.getHeaders(),
148
+ body: JSON.stringify(completionsRequest),
149
+ signal: options?.signal,
150
+ });
151
+
152
+ if (!response.ok) {
153
+ const errorText = await response.text();
154
+ throw new Error(`API error: ${response.status} ${errorText}`);
155
+ }
156
+
157
+ const reader = response.body?.getReader();
158
+ if (!reader) {
159
+ throw new Error('No response body');
160
+ }
161
+
162
+ const decoder = new TextDecoder();
163
+ let accumulated = '';
164
+ let finishReason = 'stop';
165
+
166
+ while (true) {
167
+ const { done, value } = await reader.read();
168
+ if (done) break;
169
+
170
+ const chunk = decoder.decode(value, { stream: true });
171
+ const lines = chunk.split('\n').filter(line => line.startsWith('data: '));
172
+
173
+ for (const line of lines) {
174
+ const data = line.slice(6);
175
+ if (data === '[DONE]') continue;
176
+
177
+ try {
178
+ const parsed = JSON.parse(data);
179
+ const text = parsed.choices?.[0]?.text;
180
+
181
+ if (text) {
182
+ accumulated += text;
183
+ callbacks.onChunk(text);
184
+ }
185
+
186
+ if (parsed.choices?.[0]?.finish_reason) {
187
+ finishReason = parsed.choices[0].finish_reason;
188
+ }
189
+ } catch {
190
+ // Ignore parse errors in stream
191
+ }
192
+ }
193
+ }
194
+
195
+ return this.buildStreamedResponse(accumulated, finishReason, request.model, completionsRequest);
196
+
197
+ } catch (error) {
198
+ throw this.handleError(error, completionsRequest);
199
+ }
200
+ }
201
+
202
+ // ============================================================================
203
+ // Prompt Serialization
204
+ // ============================================================================
205
+
206
+ /**
207
+ * Serialize messages to Human:/Assistant: format for base models.
208
+ * Images are stripped from content.
209
+ */
210
+ serializeToPrompt(messages: any[]): string {
211
+ const parts: string[] = [];
212
+ let hasStrippedImages = false;
213
+
214
+ for (const msg of messages) {
215
+ const role = this.normalizeRole(msg.role);
216
+ const prefix = role === 'user' ? 'Human:' : 'Assistant:';
217
+
218
+ // Extract text content, strip images
219
+ const textContent = this.extractTextContent(msg.content);
220
+ if (textContent.hadImages) {
221
+ hasStrippedImages = true;
222
+ }
223
+
224
+ if (textContent.text) {
225
+ parts.push(`${prefix} ${textContent.text}`);
226
+ }
227
+ }
228
+
229
+ if (hasStrippedImages && this.warnOnImageStrip) {
230
+ console.warn('[OpenAICompletionsAdapter] Images were stripped from context (not supported in completions mode)');
231
+ }
232
+
233
+ // Add final Assistant: prefix to prompt completion
234
+ parts.push('Assistant:');
235
+
236
+ return parts.join('\n\n');
237
+ }
238
+
239
+ private normalizeRole(role: string): 'user' | 'assistant' {
240
+ if (role === 'user' || role === 'human' || role === 'Human') {
241
+ return 'user';
242
+ }
243
+ return 'assistant';
244
+ }
245
+
246
+ private extractTextContent(content: any): { text: string; hadImages: boolean } {
247
+ if (typeof content === 'string') {
248
+ return { text: content, hadImages: false };
249
+ }
250
+
251
+ if (Array.isArray(content)) {
252
+ const textParts: string[] = [];
253
+ let hadImages = false;
254
+
255
+ for (const block of content) {
256
+ if (block.type === 'text') {
257
+ textParts.push(block.text);
258
+ } else if (block.type === 'image' || block.type === 'image_url') {
259
+ hadImages = true;
260
+ }
261
+ // Skip tool_use, tool_result, thinking blocks for base models
262
+ }
263
+
264
+ return { text: textParts.join('\n'), hadImages };
265
+ }
266
+
267
+ return { text: '', hadImages: false };
268
+ }
269
+
270
+ // ============================================================================
271
+ // Private Methods
272
+ // ============================================================================
273
+
274
+ private getHeaders(): Record<string, string> {
275
+ const headers: Record<string, string> = {
276
+ 'Content-Type': 'application/json',
277
+ ...this.extraHeaders,
278
+ };
279
+
280
+ if (this.apiKey) {
281
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
282
+ }
283
+
284
+ return headers;
285
+ }
286
+
287
+ private buildRequest(request: ProviderRequest): CompletionsRequest {
288
+ const prompt = this.serializeToPrompt(request.messages as any[]);
289
+
290
+ const params: CompletionsRequest = {
291
+ model: request.model,
292
+ prompt,
293
+ max_tokens: request.maxTokens || this.defaultMaxTokens,
294
+ };
295
+
296
+ if (request.temperature !== undefined) {
297
+ params.temperature = request.temperature;
298
+ }
299
+
300
+ // Combine default stop sequences with any provided ones
301
+ const stopSequences = [
302
+ ...this.defaultStopSequences,
303
+ ...(request.stopSequences || []),
304
+ ];
305
+ if (stopSequences.length > 0) {
306
+ params.stop = stopSequences;
307
+ }
308
+
309
+ // Apply extra params (but not messages/tools which don't apply)
310
+ if (request.extra) {
311
+ const { messages, tools, ...rest } = request.extra as any;
312
+ Object.assign(params, rest);
313
+ }
314
+
315
+ return params;
316
+ }
317
+
318
+ private async makeRequest(request: CompletionsRequest, options?: ProviderRequestOptions): Promise<CompletionsResponse> {
319
+ const response = await fetch(`${this.baseURL}/completions`, {
320
+ method: 'POST',
321
+ headers: this.getHeaders(),
322
+ body: JSON.stringify(request),
323
+ signal: options?.signal,
324
+ });
325
+
326
+ if (!response.ok) {
327
+ const errorText = await response.text();
328
+ throw new Error(`API error: ${response.status} ${errorText}`);
329
+ }
330
+
331
+ return response.json() as Promise<CompletionsResponse>;
332
+ }
333
+
334
+ private parseResponse(response: CompletionsResponse, requestedModel: string, rawRequest: unknown): ProviderResponse {
335
+ const choice = response.choices[0];
336
+ const text = choice?.text ?? '';
337
+
338
+ return {
339
+ content: this.textToContent(text),
340
+ stopReason: this.mapFinishReason(choice?.finish_reason),
341
+ stopSequence: undefined,
342
+ usage: {
343
+ inputTokens: response.usage?.prompt_tokens ?? 0,
344
+ outputTokens: response.usage?.completion_tokens ?? 0,
345
+ },
346
+ model: response.model ?? requestedModel,
347
+ rawRequest,
348
+ raw: response,
349
+ };
350
+ }
351
+
352
+ private buildStreamedResponse(
353
+ accumulated: string,
354
+ finishReason: string,
355
+ requestedModel: string,
356
+ rawRequest?: unknown
357
+ ): ProviderResponse {
358
+ return {
359
+ content: this.textToContent(accumulated),
360
+ stopReason: this.mapFinishReason(finishReason),
361
+ stopSequence: undefined,
362
+ usage: {
363
+ inputTokens: 0, // Not available in streaming
364
+ outputTokens: 0,
365
+ },
366
+ model: requestedModel,
367
+ rawRequest,
368
+ raw: { text: accumulated, finish_reason: finishReason },
369
+ };
370
+ }
371
+
372
+ private textToContent(text: string): ContentBlock[] {
373
+ // Trim leading whitespace (model often starts with space after "Assistant:")
374
+ const trimmed = text.replace(/^\s+/, '');
375
+
376
+ if (!trimmed) return [];
377
+
378
+ return [{ type: 'text', text: trimmed }];
379
+ }
380
+
381
+ private mapFinishReason(reason: string | undefined): string {
382
+ switch (reason) {
383
+ case 'stop':
384
+ return 'end_turn';
385
+ case 'length':
386
+ return 'max_tokens';
387
+ default:
388
+ return 'end_turn';
389
+ }
390
+ }
391
+
392
+ private handleError(error: unknown, rawRequest?: unknown): MembraneError {
393
+ if (error instanceof Error) {
394
+ const message = error.message;
395
+
396
+ if (message.includes('429') || message.includes('rate')) {
397
+ return rateLimitError(message, undefined, error, rawRequest);
398
+ }
399
+
400
+ if (message.includes('401') || message.includes('auth') || message.includes('Unauthorized')) {
401
+ return authError(message, error, rawRequest);
402
+ }
403
+
404
+ if (message.includes('context') || message.includes('too long') || message.includes('maximum context')) {
405
+ return contextLengthError(message, error, rawRequest);
406
+ }
407
+
408
+ if (message.includes('500') || message.includes('502') || message.includes('503')) {
409
+ return serverError(message, undefined, error, rawRequest);
410
+ }
411
+
412
+ if (error.name === 'AbortError') {
413
+ return abortError(undefined, rawRequest);
414
+ }
415
+
416
+ if (message.includes('network') || message.includes('fetch') || message.includes('ECONNREFUSED')) {
417
+ return networkError(message, error, rawRequest);
418
+ }
419
+ }
420
+
421
+ return new MembraneError({
422
+ type: 'unknown',
423
+ message: error instanceof Error ? error.message : String(error),
424
+ retryable: false,
425
+ rawError: error,
426
+ rawRequest,
427
+ });
428
+ }
429
+ }