@intx/inference-discovery-openai 0.1.2 → 0.3.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.
@@ -0,0 +1,256 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { basename } from "node:path";
3
+ import { CapabilityNotBuildableError, resolveMediaPath, } from "@intx/inference-discovery/catalog";
4
+ // gpt-5.6 Chat Completions rejects function tools unless reasoning_effort
5
+ // is explicitly "none" (use Responses API for reasoned tool use). Add a
6
+ // model here when tool calls fail with the reasoning_effort invalid_request.
7
+ const TOOL_CALL_REASONING_NONE_MODELS = new Set([
8
+ "gpt-5.6-sol",
9
+ "gpt-5.6-terra",
10
+ "gpt-5.6-luna",
11
+ ]);
12
+ function applyToolCallingEffort(body, model) {
13
+ if (TOOL_CALL_REASONING_NONE_MODELS.has(model)) {
14
+ body.reasoning_effort = "none";
15
+ }
16
+ }
17
+ function mimeTypeFor(ref) {
18
+ if (ref.kind === "image")
19
+ return "image/jpeg";
20
+ if (ref.kind === "audio")
21
+ return "audio/wav";
22
+ if (ref.kind === "video")
23
+ return "video/mp4";
24
+ if (ref.kind === "document")
25
+ return "application/pdf";
26
+ throw new Error(`unsupported media kind: ${String(ref.kind)}`);
27
+ }
28
+ function readMediaDataUri(ref) {
29
+ const abs = resolveMediaPath(ref);
30
+ const bytes = readFileSync(abs);
31
+ const mime = mimeTypeFor(ref);
32
+ return `data:${mime};base64,${bytes.toString("base64")}`;
33
+ }
34
+ function buildToolDecl(intent) {
35
+ if (!intent.tools || intent.tools.length === 0) {
36
+ throw new Error("intent has no tools but capability requires them");
37
+ }
38
+ return intent.tools.map((tool) => ({
39
+ type: "function",
40
+ function: {
41
+ name: tool.name,
42
+ description: tool.description,
43
+ parameters: tool.parameters,
44
+ },
45
+ }));
46
+ }
47
+ function buildPlainTextBody(model, intent, stream) {
48
+ const body = {
49
+ model,
50
+ messages: [{ role: "user", content: intent.prompt }],
51
+ };
52
+ if (stream)
53
+ body.stream = true;
54
+ return body;
55
+ }
56
+ function buildFunctionCallingBody(model, intent) {
57
+ const body = {
58
+ model,
59
+ messages: [{ role: "user", content: intent.prompt }],
60
+ tools: buildToolDecl(intent),
61
+ };
62
+ applyToolCallingEffort(body, model);
63
+ return body;
64
+ }
65
+ export function buildMultiTurnTurn1Body(opts) {
66
+ const body = {
67
+ model: opts.model,
68
+ messages: [{ role: "user", content: opts.intent.prompt }],
69
+ tools: buildToolDecl(opts.intent),
70
+ };
71
+ applyToolCallingEffort(body, opts.model);
72
+ return body;
73
+ }
74
+ function isRecord(value) {
75
+ return typeof value === "object" && value !== null && !Array.isArray(value);
76
+ }
77
+ function findToolFollowUp(intent) {
78
+ const followUp = intent.followUp;
79
+ if (followUp === undefined || followUp.length === 0) {
80
+ throw new Error("multi-turn: intent has no followUp entries");
81
+ }
82
+ for (const step of followUp) {
83
+ if (step.role === "tool") {
84
+ return { toolName: step.toolName, content: step.content };
85
+ }
86
+ }
87
+ throw new Error("multi-turn: intent.followUp has no role:'tool' entry");
88
+ }
89
+ function extractAssistantMessage(parsed) {
90
+ if (!isRecord(parsed)) {
91
+ throw new Error("multi-turn: turn-1 response is not a JSON object");
92
+ }
93
+ const choices = parsed.choices;
94
+ if (!Array.isArray(choices) || choices.length === 0) {
95
+ throw new Error("multi-turn: turn-1 response has no choices array");
96
+ }
97
+ const first = choices[0];
98
+ if (!isRecord(first)) {
99
+ throw new Error("multi-turn: turn-1 response choices[0] is not an object");
100
+ }
101
+ const message = first.message;
102
+ if (!isRecord(message)) {
103
+ throw new Error("multi-turn: turn-1 response choices[0].message is not an object");
104
+ }
105
+ return message;
106
+ }
107
+ function extractFirstToolCallId(message) {
108
+ const toolCalls = message.tool_calls;
109
+ if (!Array.isArray(toolCalls) || toolCalls.length === 0) {
110
+ throw new Error("multi-turn: turn-1 assistant message has no tool_calls");
111
+ }
112
+ const first = toolCalls[0];
113
+ if (!isRecord(first)) {
114
+ throw new Error("multi-turn: turn-1 tool_calls[0] is not an object");
115
+ }
116
+ const id = first.id;
117
+ if (typeof id !== "string" || id.length === 0) {
118
+ throw new Error("multi-turn: turn-1 tool_calls[0].id is not a string");
119
+ }
120
+ return id;
121
+ }
122
+ export function buildMultiTurnTurn2Body(opts) {
123
+ const assistantMessage = extractAssistantMessage(opts.turn1Response);
124
+ const toolCallId = extractFirstToolCallId(assistantMessage);
125
+ const tool = findToolFollowUp(opts.intent);
126
+ const messages = [
127
+ ...opts.turn1Body.messages,
128
+ assistantMessage,
129
+ {
130
+ role: "tool",
131
+ tool_call_id: toolCallId,
132
+ content: tool.content,
133
+ },
134
+ ];
135
+ const tools = opts.turn1Body.tools;
136
+ const body = {
137
+ model: opts.model,
138
+ messages,
139
+ };
140
+ if (tools !== undefined) {
141
+ body.tools = tools;
142
+ }
143
+ applyToolCallingEffort(body, opts.model);
144
+ return body;
145
+ }
146
+ function buildReasoningBody(model, intent, stream) {
147
+ const body = {
148
+ model,
149
+ messages: [{ role: "user", content: intent.prompt }],
150
+ };
151
+ if (stream)
152
+ body.stream = true;
153
+ return body;
154
+ }
155
+ // Translate the intent's responseFormat to OpenAI's response_format
156
+ // wire field. Mirrors toOpenAIResponseFormat in the inference adapter
157
+ // (packages/inference/src/providers/openai.ts); duplicated here
158
+ // because the discovery plug-in builds wire requests directly without
159
+ // running through the adapter. Kept in sync by the shared
160
+ // CapabilityIntent shape.
161
+ function toOpenAIResponseFormat(format) {
162
+ switch (format.kind) {
163
+ case "text":
164
+ return { type: "text" };
165
+ case "json":
166
+ return { type: "json_object" };
167
+ case "json-schema": {
168
+ const jsonSchema = {
169
+ name: format.name,
170
+ schema: format.schema,
171
+ };
172
+ if (format.strict !== undefined)
173
+ jsonSchema["strict"] = format.strict;
174
+ return { type: "json_schema", json_schema: jsonSchema };
175
+ }
176
+ }
177
+ }
178
+ function buildStructuredOutputBody(model, intent, stream) {
179
+ if (intent.responseFormat === undefined) {
180
+ throw new Error("OpenAI protocol: structured-output intent has no responseFormat");
181
+ }
182
+ const body = {
183
+ model,
184
+ messages: [{ role: "user", content: intent.prompt }],
185
+ response_format: toOpenAIResponseFormat(intent.responseFormat),
186
+ };
187
+ if (stream)
188
+ body.stream = true;
189
+ return body;
190
+ }
191
+ // Shared skeleton for Chat Completions multimodal user turns: text
192
+ // prompt plus one typed part per media ref. Vision and document only
193
+ // differ in the expected kind and the part shape.
194
+ function buildMediaInputBody(model, intent, capability, expectedKind, toPart) {
195
+ if (!intent.media || intent.media.length === 0) {
196
+ throw new Error(`intent has no media but ${capability} requires it`);
197
+ }
198
+ const parts = [{ type: "text", text: intent.prompt }];
199
+ for (const ref of intent.media) {
200
+ if (ref.kind !== expectedKind) {
201
+ throw new Error(`${capability} only accepts ${expectedKind} media, got: ${ref.kind}`);
202
+ }
203
+ parts.push(toPart(ref));
204
+ }
205
+ return {
206
+ model,
207
+ messages: [{ role: "user", content: parts }],
208
+ };
209
+ }
210
+ function imagePart(ref) {
211
+ return {
212
+ type: "image_url",
213
+ image_url: { url: readMediaDataUri(ref) },
214
+ };
215
+ }
216
+ function documentPart(ref) {
217
+ const filename = basename(ref.path);
218
+ if (filename.length === 0) {
219
+ throw new Error("document-input media path has an empty basename; Chat Completions requires filename");
220
+ }
221
+ return {
222
+ type: "file",
223
+ file: {
224
+ filename,
225
+ file_data: readMediaDataUri(ref),
226
+ },
227
+ };
228
+ }
229
+ export function buildRequestBody(args) {
230
+ const { model, capability, intent } = args;
231
+ switch (capability) {
232
+ case "plain-text":
233
+ return buildPlainTextBody(model, intent, false);
234
+ case "plain-text-streaming":
235
+ return buildPlainTextBody(model, intent, true);
236
+ case "function-calling":
237
+ return buildFunctionCallingBody(model, intent);
238
+ case "function-calling-multi-turn":
239
+ throw new Error("function-calling-multi-turn is a multi-step capability; use buildMultiTurnTurn1Body / buildMultiTurnTurn2Body via iterateCaptureSteps");
240
+ case "reasoning-content":
241
+ return buildReasoningBody(model, intent, false);
242
+ case "reasoning-content-streaming":
243
+ return buildReasoningBody(model, intent, true);
244
+ case "vision-input":
245
+ return buildMediaInputBody(model, intent, "vision-input", "image", imagePart);
246
+ case "document-input":
247
+ return buildMediaInputBody(model, intent, "document-input", "document", documentPart);
248
+ case "structured-output":
249
+ return buildStructuredOutputBody(model, intent, false);
250
+ case "structured-output-streaming":
251
+ case "structured-output-refusal-streaming":
252
+ return buildStructuredOutputBody(model, intent, true);
253
+ default:
254
+ throw new CapabilityNotBuildableError(capability, `OpenAI protocol: capability "${capability}" not implemented`);
255
+ }
256
+ }
@@ -0,0 +1 @@
1
+ export declare function buildEndpointURL(baseUrl: string): string;
@@ -0,0 +1,3 @@
1
+ export function buildEndpointURL(baseUrl) {
2
+ return `${baseUrl}/chat/completions`;
3
+ }
@@ -0,0 +1,2 @@
1
+ import type { CaptureStep, CapturedResponse, IterateCaptureStepsOpts } from "@intx/inference-discovery";
2
+ export declare function createOpenaiIterator(baseUrl: string): (opts: IterateCaptureStepsOpts) => Generator<CaptureStep, void, CapturedResponse>;
@@ -0,0 +1,37 @@
1
+ import {} from "@intx/inference-discovery/catalog";
2
+ import { buildEndpointURL } from "./endpoint.js";
3
+ import { buildMultiTurnTurn1Body, buildMultiTurnTurn2Body, buildRequestBody, } from "./body.js";
4
+ const MULTI_TURN_CAPABILITIES = new Set([
5
+ "function-calling-multi-turn",
6
+ ]);
7
+ export function createOpenaiIterator(baseUrl) {
8
+ return function* iterateCaptureSteps(opts) {
9
+ const { model, capability, intent } = opts;
10
+ const url = buildEndpointURL(baseUrl);
11
+ if (MULTI_TURN_CAPABILITIES.has(capability)) {
12
+ const turn1 = buildMultiTurnTurn1Body({ model, intent });
13
+ const turn1Response = yield {
14
+ kind: "json",
15
+ url,
16
+ body: turn1,
17
+ };
18
+ const turn2 = buildMultiTurnTurn2Body({
19
+ model,
20
+ intent,
21
+ turn1Body: turn1,
22
+ turn1Response: turn1Response.parsed,
23
+ });
24
+ yield {
25
+ kind: "json",
26
+ url,
27
+ body: turn2,
28
+ };
29
+ return;
30
+ }
31
+ yield {
32
+ kind: "json",
33
+ url,
34
+ body: buildRequestBody({ model, capability, intent }),
35
+ };
36
+ };
37
+ }
package/package.json CHANGED
@@ -1,16 +1,27 @@
1
1
  {
2
2
  "name": "@intx/inference-discovery-openai",
3
- "version": "0.1.2",
3
+ "description": "OpenAI-protocol provider plug-in for the inference discovery rig",
4
+ "version": "0.3.0",
4
5
  "license": "LGPL-2.1-only",
5
6
  "type": "module",
6
7
  "exports": {
7
8
  ".": {
8
- "types": "./src/index.ts",
9
- "default": "./src/index.ts"
9
+ "intx-src": "./src/index.ts",
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
10
12
  }
11
13
  },
12
14
  "dependencies": {
13
- "@intx/inference-discovery": "0.0.0",
15
+ "@intx/inference-discovery": "0.3.0",
14
16
  "arktype": "^2.1.29"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "sideEffects": false,
24
+ "publishConfig": {
25
+ "access": "public"
15
26
  }
16
27
  }
@@ -1,93 +0,0 @@
1
- import type { ProviderPlugin } from "@intx/inference-discovery";
2
- import { buildAuthHeaders } from "../protocol/auth";
3
- import { createOpenaiIterator } from "../protocol/iterator";
4
-
5
- const PROVIDER_NAME = "opencode-zen";
6
-
7
- const OPENCODE_ZEN_MODELS: readonly string[] = [
8
- "kimi-k2.6",
9
- "glm-5.1",
10
- "deepseek-v4-pro",
11
- "qwen3.6-plus",
12
- "mimo-v2-omni",
13
- ];
14
-
15
- const REDACT_REQUEST_HEADERS: readonly string[] = ["authorization"];
16
- const REDACT_RESPONSE_HEADERS: readonly string[] = [
17
- "set-cookie",
18
- "x-request-id",
19
- ];
20
-
21
- function isRecord(value: unknown): value is Record<string, unknown> {
22
- return typeof value === "object" && value !== null && !Array.isArray(value);
23
- }
24
-
25
- function lookupPath(
26
- value: unknown,
27
- path: readonly (string | number)[],
28
- ): unknown {
29
- let cursor: unknown = value;
30
- for (const segment of path) {
31
- if (cursor === null || cursor === undefined) return undefined;
32
- if (typeof segment === "number") {
33
- if (!Array.isArray(cursor)) return undefined;
34
- cursor = cursor[segment];
35
- } else {
36
- if (!isRecord(cursor)) return undefined;
37
- cursor = cursor[segment];
38
- }
39
- }
40
- return cursor;
41
- }
42
-
43
- function isNonEmpty(value: unknown): boolean {
44
- if (value === null || value === undefined) return false;
45
- if (typeof value === "string") return value.length > 0;
46
- if (Array.isArray(value)) return value.length > 0;
47
- if (typeof value === "object") return Object.keys(value).length > 0;
48
- return true;
49
- }
50
-
51
- export interface ReasoningTrace {
52
- fieldPath: string;
53
- sample: unknown;
54
- }
55
-
56
- // kimi-k2.6 silently routes between two upstream backends that emit
57
- // reasoning under different field paths. Recording which path a given
58
- // capture hit is the cheapest way to detect routing changes later.
59
- const REASONING_FIELD_PATHS: readonly (readonly (string | number)[])[] = [
60
- ["choices", 0, "message", "reasoning_content"],
61
- ["choices", 0, "message", "reasoning"],
62
- ["choices", 0, "message", "reasoning_details"],
63
- ];
64
-
65
- export function extractReasoningTrace(parsed: unknown): ReasoningTrace | null {
66
- for (const path of REASONING_FIELD_PATHS) {
67
- const value = lookupPath(parsed, path);
68
- if (isNonEmpty(value)) {
69
- return { fieldPath: path.join("."), sample: value };
70
- }
71
- }
72
- return null;
73
- }
74
-
75
- export interface CreateOpencodeZenPluginOpts {
76
- apiKey: string;
77
- baseUrl: string;
78
- }
79
-
80
- export function createOpencodeZenPlugin(
81
- opts: CreateOpencodeZenPluginOpts,
82
- ): ProviderPlugin {
83
- const { apiKey, baseUrl } = opts;
84
- return {
85
- name: PROVIDER_NAME,
86
- models: OPENCODE_ZEN_MODELS,
87
- redactRequestHeaders: REDACT_REQUEST_HEADERS,
88
- redactResponseHeaders: REDACT_RESPONSE_HEADERS,
89
- buildAuthHeaders: () => buildAuthHeaders(apiKey),
90
- extractReasoningTrace,
91
- iterateCaptureSteps: createOpenaiIterator(baseUrl),
92
- };
93
- }
package/src/index.ts DELETED
@@ -1,6 +0,0 @@
1
- export {
2
- createOpencodeZenPlugin,
3
- extractReasoningTrace,
4
- type CreateOpencodeZenPluginOpts,
5
- type ReasoningTrace,
6
- } from "./deployments/opencode-zen";