@intx/inference-discovery-openai 0.2.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.
package/README.md CHANGED
@@ -10,8 +10,8 @@ is organised in two layers:
10
10
  - `deployments/` — concrete deployments built on the protocol
11
11
  layer. Each deployment names a provider, lists its models,
12
12
  declares its auth and redaction policy, and (where needed)
13
- overrides reasoning extraction. Today the only deployment is
14
- OpenCode Zen.
13
+ overrides reasoning extraction. Three deployments ship today: the
14
+ OpenCode Zen relay, first-party OpenAI, and first-party xAI.
15
15
 
16
16
  See [`@intx/inference-discovery`](../inference-discovery/README.md)
17
17
  for the runtime, the plug-in contract, and the `discover` CLI.
@@ -30,41 +30,98 @@ re-run unchanged against it.
30
30
  import { createOpencodeZenPlugin } from "@intx/inference-discovery-openai";
31
31
 
32
32
  const plugin = createOpencodeZenPlugin({
33
- apiKey: process.env.OPENAI_API_KEY,
34
- baseUrl: process.env.OPENAI_BASE_URL,
33
+ apiKey: process.env.OPENCODE_API_KEY,
34
+ baseUrl: process.env.OPENCODE_BASE_URL,
35
35
  });
36
36
  // Hand off to runCapture from @intx/inference-discovery.
37
37
  ```
38
38
 
39
- Models: `kimi-k2.6`, `glm-5.1`, `deepseek-v4-pro`, `qwen3.6-plus`,
40
- `mimo-v2-omni`.
39
+ Models: `kimi-k2.6`, `kimi-k2.7-code`, `kimi-k3`, `glm-5.2`,
40
+ `deepseek-v4-pro`, `deepseek-v4-flash`, `qwen3.7-plus`, `mimo-v2.5`,
41
+ `gpt-5.4-mini`.
41
42
 
42
43
  For the per-model, per-capability behaviour observed at capture
43
44
  time — including the discrepancies between vendor documentation
44
45
  and the actual wire bytes — see
45
- [`docs/OPENCODE_DISCOVERY.md`](../../docs/OPENCODE_DISCOVERY.md).
46
- The matrix entries for this deployment live in `SUPPORT_MATRIX`;
47
- two vision entries are marked `refused` and `http-error` and so
48
- produce no fixtures.
49
-
50
- ### Reasoning trace extraction
51
-
52
- OpenCode Zen routes `kimi-k2.6` between two upstream backends that
53
- emit reasoning content under different field paths. The deployment
54
- ships a reasoning extractor that probes the known paths and records
55
- which one held the non-empty value. For non-streaming reasoning
56
- captures the runner writes the result to `reasoning-trace.json`
57
- next to the response so a later routing change is detectable from
58
- the fixtures alone; streaming reasoning captures do not get the
59
- sidecar (the runner does not parse SSE bodies), and the routing
60
- signal lives in the captured event stream itself.
46
+ [`docs/discovery.md`](./docs/discovery.md).
47
+ The matrix entries for this deployment live in `SUPPORT_MATRIX`.
48
+ `deepseek-v4-pro` vision-input is `http-error` and produces no
49
+ fixture; other non-vision models simply omit vision rows.
61
50
 
62
51
  ### Environment
63
52
 
64
- | Variable | Purpose |
65
- | ----------------- | ------------------------------------------------------------ |
66
- | `OPENAI_API_KEY` | Sent as `Authorization: Bearer <key>`. Redacted in fixtures. |
67
- | `OPENAI_BASE_URL` | Relay base URL (e.g. `https://opencode.ai/zen/v1`). |
53
+ | Variable | Purpose |
54
+ | ------------------- | ------------------------------------------------------------ |
55
+ | `OPENCODE_API_KEY` | Sent as `Authorization: Bearer <key>`. Redacted in fixtures. |
56
+ | `OPENCODE_BASE_URL` | Relay base URL (e.g. `https://opencode.ai/zen/v1`). |
57
+
58
+ ## OpenAI
59
+
60
+ The `openai` deployment probes first-party `api.openai.com` directly,
61
+ under provider name `openai` (distinct from `opencode-zen`, though both
62
+ write sessions into this package's `sessions/` tree). The base URL is fixed
63
+ to `https://api.openai.com/v1`; the deployment reads only `OPENAI_API_KEY`.
64
+
65
+ ```ts
66
+ import { createOpenAIPlugin } from "@intx/inference-discovery-openai";
67
+
68
+ const plugin = createOpenAIPlugin({ apiKey: process.env.OPENAI_API_KEY });
69
+ // Hand off to runCapture from @intx/inference-discovery.
70
+ ```
71
+
72
+ Models: `gpt-5.5`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`.
73
+
74
+ Each first-party model captures `plain-text`, `function-calling`,
75
+ `function-calling-multi-turn`, `vision-input`, `document-input`, and
76
+ `structured-output` (plus the streaming variants the OpenAI-protocol body
77
+ builder emits). `reasoning-content` is marked `unsupported`: first-party
78
+ `api.openai.com` Chat Completions responses carry no reasoning field for
79
+ the gpt-5 series (OpenAI surfaces reasoning only via the Responses API,
80
+ which this plug-in does not probe). The `function-calling-multi-turn`,
81
+ `vision-input`, and `document-input` streaming variants carry no rows
82
+ because the body builder does not build them — a rig gap, not a provider
83
+ limitation.
84
+
85
+ ### Environment
86
+
87
+ | Variable | Purpose |
88
+ | ---------------- | ------------------------------------------------------------ |
89
+ | `OPENAI_API_KEY` | Sent as `Authorization: Bearer <key>`. Redacted in fixtures. |
90
+
91
+ ## xAI
92
+
93
+ The `xai` deployment probes first-party `api.x.ai` directly, under provider
94
+ name `xai` (distinct from `openai` and `opencode-zen`, though all three write
95
+ sessions into this package's `sessions/` tree). The base URL is fixed to
96
+ `https://api.x.ai/v1`; the deployment reads only `XAI_API_KEY`.
97
+
98
+ ```ts
99
+ import { createXaiPlugin } from "@intx/inference-discovery-openai";
100
+
101
+ const plugin = createXaiPlugin({ apiKey: process.env.XAI_API_KEY });
102
+ // Hand off to runCapture from @intx/inference-discovery.
103
+ ```
104
+
105
+ Models: `grok-4.20-0309-non-reasoning`, `grok-4.20-0309-reasoning`, `grok-4.3`,
106
+ `grok-4.5`, `grok-4.6`, `grok-build-0.1`.
107
+
108
+ Each model captures `plain-text`, `function-calling`,
109
+ `function-calling-multi-turn`, `vision-input`, and `structured-output` (plus
110
+ the streaming variants the OpenAI-protocol body builder emits).
111
+ `reasoning-content` captures on every model except
112
+ `grok-4.20-0309-non-reasoning`, xAI's explicit non-reasoning variant, which
113
+ returns text with no `reasoning_content` field and so is marked `unsupported`.
114
+ `structured-output-refusal-streaming` is `misled`: under a strict `json_schema`
115
+ plus a declining prompt, the model streams schema-conformant JSON carrying a
116
+ textual decline rather than a `delta.refusal` field. `document-input` is
117
+ `http-error`: xAI Chat Completions rejects file content parts and directs
118
+ callers to the Responses API, which this plug-in does not probe.
119
+
120
+ ### Environment
121
+
122
+ | Variable | Purpose |
123
+ | ------------- | ------------------------------------------------------------ |
124
+ | `XAI_API_KEY` | Sent as `Authorization: Bearer <key>`. Redacted in fixtures. |
68
125
 
69
126
  ## Adding a new deployment
70
127
 
@@ -0,0 +1,5 @@
1
+ import type { ProviderPlugin } from "@intx/inference-discovery";
2
+ export interface CreateOpenAIPluginOpts {
3
+ apiKey: string;
4
+ }
5
+ export declare function createOpenAIPlugin(opts: CreateOpenAIPluginOpts): ProviderPlugin;
@@ -0,0 +1,53 @@
1
+ import { buildAuthHeaders } from "../protocol/auth.js";
2
+ import { createOpenaiIterator } from "../protocol/iterator.js";
3
+ const PROVIDER_NAME = "openai";
4
+ // First-party OpenAI Chat Completions endpoint. Unlike the OpenCode Zen relay,
5
+ // there is no configurable base URL: the deployment reads only OPENAI_API_KEY
6
+ // and always talks to api.openai.com.
7
+ const OPENAI_BASE_URL = "https://api.openai.com/v1";
8
+ const OPENAI_MODELS = [
9
+ "gpt-5.5",
10
+ "gpt-5.6-sol",
11
+ "gpt-5.6-terra",
12
+ "gpt-5.6-luna",
13
+ "gpt-5",
14
+ "gpt-5-mini",
15
+ "gpt-5-nano",
16
+ "gpt-5.1",
17
+ "gpt-5.2",
18
+ "gpt-5.4",
19
+ "gpt-5.4-mini",
20
+ "gpt-5.4-nano",
21
+ "o1",
22
+ "o3",
23
+ "o3-mini",
24
+ "o4-mini",
25
+ "gpt-4",
26
+ "gpt-4-turbo",
27
+ "gpt-4.1",
28
+ "gpt-4.1-mini",
29
+ "gpt-4.1-nano",
30
+ "gpt-4o",
31
+ "gpt-4o-mini",
32
+ ];
33
+ const REDACT_REQUEST_HEADERS = ["authorization"];
34
+ const REDACT_RESPONSE_HEADERS = [
35
+ "set-cookie",
36
+ "x-request-id",
37
+ "openai-organization",
38
+ "openai-project",
39
+ ];
40
+ // A live gpt-5.5 capture confirmed that first-party api.openai.com Chat
41
+ // Completions responses carry no reasoning or reasoning_content field: OpenAI
42
+ // surfaces reasoning only via the Responses API, not this Chat Completions wire.
43
+ export function createOpenAIPlugin(opts) {
44
+ const { apiKey } = opts;
45
+ return {
46
+ name: PROVIDER_NAME,
47
+ models: OPENAI_MODELS,
48
+ redactRequestHeaders: REDACT_REQUEST_HEADERS,
49
+ redactResponseHeaders: REDACT_RESPONSE_HEADERS,
50
+ buildAuthHeaders: () => buildAuthHeaders(apiKey),
51
+ iterateCaptureSteps: createOpenaiIterator(OPENAI_BASE_URL),
52
+ };
53
+ }
@@ -1,9 +1,4 @@
1
1
  import type { ProviderPlugin } from "@intx/inference-discovery";
2
- export interface ReasoningTrace {
3
- fieldPath: string;
4
- sample: unknown;
5
- }
6
- export declare function extractReasoningTrace(parsed: unknown): ReasoningTrace | null;
7
2
  export interface CreateOpencodeZenPluginOpts {
8
3
  apiKey: string;
9
4
  baseUrl: string;
@@ -3,65 +3,20 @@ import { createOpenaiIterator } from "../protocol/iterator.js";
3
3
  const PROVIDER_NAME = "opencode-zen";
4
4
  const OPENCODE_ZEN_MODELS = [
5
5
  "kimi-k2.6",
6
- "glm-5.1",
6
+ "kimi-k2.7-code",
7
+ "kimi-k3",
8
+ "glm-5.2",
7
9
  "deepseek-v4-pro",
8
- "qwen3.6-plus",
9
- "mimo-v2-omni",
10
+ "deepseek-v4-flash",
11
+ "qwen3.7-plus",
12
+ "mimo-v2.5",
13
+ "gpt-5.4-mini",
10
14
  ];
11
15
  const REDACT_REQUEST_HEADERS = ["authorization"];
12
16
  const REDACT_RESPONSE_HEADERS = [
13
17
  "set-cookie",
14
18
  "x-request-id",
15
19
  ];
16
- function isRecord(value) {
17
- return typeof value === "object" && value !== null && !Array.isArray(value);
18
- }
19
- function lookupPath(value, path) {
20
- let cursor = value;
21
- for (const segment of path) {
22
- if (cursor === null || cursor === undefined)
23
- return undefined;
24
- if (typeof segment === "number") {
25
- if (!Array.isArray(cursor))
26
- return undefined;
27
- cursor = cursor[segment];
28
- }
29
- else {
30
- if (!isRecord(cursor))
31
- return undefined;
32
- cursor = cursor[segment];
33
- }
34
- }
35
- return cursor;
36
- }
37
- function isNonEmpty(value) {
38
- if (value === null || value === undefined)
39
- return false;
40
- if (typeof value === "string")
41
- return value.length > 0;
42
- if (Array.isArray(value))
43
- return value.length > 0;
44
- if (typeof value === "object")
45
- return Object.keys(value).length > 0;
46
- return true;
47
- }
48
- // kimi-k2.6 silently routes between two upstream backends that emit
49
- // reasoning under different field paths. Recording which path a given
50
- // capture hit is the cheapest way to detect routing changes later.
51
- const REASONING_FIELD_PATHS = [
52
- ["choices", 0, "message", "reasoning_content"],
53
- ["choices", 0, "message", "reasoning"],
54
- ["choices", 0, "message", "reasoning_details"],
55
- ];
56
- export function extractReasoningTrace(parsed) {
57
- for (const path of REASONING_FIELD_PATHS) {
58
- const value = lookupPath(parsed, path);
59
- if (isNonEmpty(value)) {
60
- return { fieldPath: path.join("."), sample: value };
61
- }
62
- }
63
- return null;
64
- }
65
20
  export function createOpencodeZenPlugin(opts) {
66
21
  const { apiKey, baseUrl } = opts;
67
22
  return {
@@ -70,7 +25,6 @@ export function createOpencodeZenPlugin(opts) {
70
25
  redactRequestHeaders: REDACT_REQUEST_HEADERS,
71
26
  redactResponseHeaders: REDACT_RESPONSE_HEADERS,
72
27
  buildAuthHeaders: () => buildAuthHeaders(apiKey),
73
- extractReasoningTrace,
74
28
  iterateCaptureSteps: createOpenaiIterator(baseUrl),
75
29
  };
76
30
  }
@@ -0,0 +1,5 @@
1
+ import type { ProviderPlugin } from "@intx/inference-discovery";
2
+ export interface CreateXaiPluginOpts {
3
+ apiKey: string;
4
+ }
5
+ export declare function createXaiPlugin(opts: CreateXaiPluginOpts): ProviderPlugin;
@@ -0,0 +1,35 @@
1
+ import { buildAuthHeaders } from "../protocol/auth.js";
2
+ import { createOpenaiIterator } from "../protocol/iterator.js";
3
+ const PROVIDER_NAME = "xai";
4
+ // First-party xAI Chat Completions endpoint. Like the first-party OpenAI
5
+ // deployment and unlike the OpenCode Zen relay, there is no configurable base
6
+ // URL: the deployment reads only XAI_API_KEY and always talks to api.x.ai. This
7
+ // string must stay byte-identical to CATALOG_TO_BASE_URL["xai"] in
8
+ // @intx/inference-discovery's provider-adapter, which stamps the manifest's
9
+ // source.baseURL; the request URL comes from here and the recorded provenance
10
+ // comes from there, and nothing cross-checks them at capture time.
11
+ const XAI_BASE_URL = "https://api.x.ai/v1";
12
+ const XAI_MODELS = [
13
+ "grok-4.20-0309-non-reasoning",
14
+ "grok-4.20-0309-reasoning",
15
+ "grok-4.3",
16
+ "grok-4.5",
17
+ "grok-4.6",
18
+ "grok-build-0.1",
19
+ ];
20
+ const REDACT_REQUEST_HEADERS = ["authorization"];
21
+ const REDACT_RESPONSE_HEADERS = [
22
+ "set-cookie",
23
+ "x-request-id",
24
+ ];
25
+ export function createXaiPlugin(opts) {
26
+ const { apiKey } = opts;
27
+ return {
28
+ name: PROVIDER_NAME,
29
+ models: XAI_MODELS,
30
+ redactRequestHeaders: REDACT_REQUEST_HEADERS,
31
+ redactResponseHeaders: REDACT_RESPONSE_HEADERS,
32
+ buildAuthHeaders: () => buildAuthHeaders(apiKey),
33
+ iterateCaptureSteps: createOpenaiIterator(XAI_BASE_URL),
34
+ };
35
+ }
package/dist/index.d.ts CHANGED
@@ -1 +1,3 @@
1
- export { createOpencodeZenPlugin, extractReasoningTrace, type CreateOpencodeZenPluginOpts, type ReasoningTrace, } from "./deployments/opencode-zen.js";
1
+ export { createOpencodeZenPlugin, type CreateOpencodeZenPluginOpts, } from "./deployments/opencode-zen.js";
2
+ export { createOpenAIPlugin, type CreateOpenAIPluginOpts, } from "./deployments/openai.js";
3
+ export { createXaiPlugin, type CreateXaiPluginOpts } from "./deployments/xai.js";
package/dist/index.js CHANGED
@@ -1 +1,3 @@
1
- export { createOpencodeZenPlugin, extractReasoningTrace, } from "./deployments/opencode-zen.js";
1
+ export { createOpencodeZenPlugin, } from "./deployments/opencode-zen.js";
2
+ export { createOpenAIPlugin, } from "./deployments/openai.js";
3
+ export { createXaiPlugin } from "./deployments/xai.js";
@@ -20,6 +20,7 @@ export interface ChatCompletionsRequest {
20
20
  stream?: boolean;
21
21
  tools?: unknown[];
22
22
  response_format?: unknown;
23
+ reasoning_effort?: "none";
23
24
  }
24
25
  export declare function buildMultiTurnTurn1Body(opts: {
25
26
  model: string;
@@ -1,5 +1,19 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { resolveMediaPath, } from "@intx/inference-discovery/catalog";
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
+ }
3
17
  function mimeTypeFor(ref) {
4
18
  if (ref.kind === "image")
5
19
  return "image/jpeg";
@@ -40,18 +54,22 @@ function buildPlainTextBody(model, intent, stream) {
40
54
  return body;
41
55
  }
42
56
  function buildFunctionCallingBody(model, intent) {
43
- return {
57
+ const body = {
44
58
  model,
45
59
  messages: [{ role: "user", content: intent.prompt }],
46
60
  tools: buildToolDecl(intent),
47
61
  };
62
+ applyToolCallingEffort(body, model);
63
+ return body;
48
64
  }
49
65
  export function buildMultiTurnTurn1Body(opts) {
50
- return {
66
+ const body = {
51
67
  model: opts.model,
52
68
  messages: [{ role: "user", content: opts.intent.prompt }],
53
69
  tools: buildToolDecl(opts.intent),
54
70
  };
71
+ applyToolCallingEffort(body, opts.model);
72
+ return body;
55
73
  }
56
74
  function isRecord(value) {
57
75
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -122,6 +140,7 @@ export function buildMultiTurnTurn2Body(opts) {
122
140
  if (tools !== undefined) {
123
141
  body.tools = tools;
124
142
  }
143
+ applyToolCallingEffort(body, opts.model);
125
144
  return body;
126
145
  }
127
146
  function buildReasoningBody(model, intent, stream) {
@@ -169,25 +188,44 @@ function buildStructuredOutputBody(model, intent, stream) {
169
188
  body.stream = true;
170
189
  return body;
171
190
  }
172
- function buildVisionBody(model, intent) {
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) {
173
195
  if (!intent.media || intent.media.length === 0) {
174
- throw new Error("intent has no media but vision-input requires it");
196
+ throw new Error(`intent has no media but ${capability} requires it`);
175
197
  }
176
198
  const parts = [{ type: "text", text: intent.prompt }];
177
199
  for (const ref of intent.media) {
178
- if (ref.kind !== "image") {
179
- throw new Error(`vision-input only accepts image media, got: ${ref.kind}`);
200
+ if (ref.kind !== expectedKind) {
201
+ throw new Error(`${capability} only accepts ${expectedKind} media, got: ${ref.kind}`);
180
202
  }
181
- parts.push({
182
- type: "image_url",
183
- image_url: { url: readMediaDataUri(ref) },
184
- });
203
+ parts.push(toPart(ref));
185
204
  }
186
205
  return {
187
206
  model,
188
207
  messages: [{ role: "user", content: parts }],
189
208
  };
190
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
+ }
191
229
  export function buildRequestBody(args) {
192
230
  const { model, capability, intent } = args;
193
231
  switch (capability) {
@@ -204,12 +242,15 @@ export function buildRequestBody(args) {
204
242
  case "reasoning-content-streaming":
205
243
  return buildReasoningBody(model, intent, true);
206
244
  case "vision-input":
207
- return buildVisionBody(model, intent);
245
+ return buildMediaInputBody(model, intent, "vision-input", "image", imagePart);
246
+ case "document-input":
247
+ return buildMediaInputBody(model, intent, "document-input", "document", documentPart);
208
248
  case "structured-output":
209
249
  return buildStructuredOutputBody(model, intent, false);
210
250
  case "structured-output-streaming":
251
+ case "structured-output-refusal-streaming":
211
252
  return buildStructuredOutputBody(model, intent, true);
212
253
  default:
213
- throw new Error(`OpenAI protocol: capability "${capability}" not implemented`);
254
+ throw new CapabilityNotBuildableError(capability, `OpenAI protocol: capability "${capability}" not implemented`);
214
255
  }
215
256
  }
@@ -12,7 +12,6 @@ export function createOpenaiIterator(baseUrl) {
12
12
  const turn1 = buildMultiTurnTurn1Body({ model, intent });
13
13
  const turn1Response = yield {
14
14
  kind: "json",
15
- subdir: "turn-1",
16
15
  url,
17
16
  body: turn1,
18
17
  };
@@ -24,7 +23,6 @@ export function createOpenaiIterator(baseUrl) {
24
23
  });
25
24
  yield {
26
25
  kind: "json",
27
- subdir: "turn-2",
28
26
  url,
29
27
  body: turn2,
30
28
  };
@@ -32,7 +30,6 @@ export function createOpenaiIterator(baseUrl) {
32
30
  }
33
31
  yield {
34
32
  kind: "json",
35
- subdir: null,
36
33
  url,
37
34
  body: buildRequestBody({ model, capability, intent }),
38
35
  };
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@intx/inference-discovery-openai",
3
- "version": "0.2.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": {
@@ -11,7 +12,7 @@
11
12
  }
12
13
  },
13
14
  "dependencies": {
14
- "@intx/inference-discovery": "0.2.2",
15
+ "@intx/inference-discovery": "0.3.0",
15
16
  "arktype": "^2.1.29"
16
17
  },
17
18
  "files": [