@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 +83 -26
- package/dist/deployments/openai.d.ts +5 -0
- package/dist/deployments/openai.js +53 -0
- package/dist/deployments/opencode-zen.d.ts +0 -5
- package/dist/deployments/opencode-zen.js +7 -53
- package/dist/deployments/xai.d.ts +5 -0
- package/dist/deployments/xai.js +35 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/protocol/body.d.ts +1 -0
- package/dist/protocol/body.js +54 -13
- package/dist/protocol/iterator.js +0 -3
- package/package.json +3 -2
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.
|
|
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.
|
|
34
|
-
baseUrl: process.env.
|
|
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`, `
|
|
40
|
-
`mimo-v2
|
|
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/
|
|
46
|
-
The matrix entries for this deployment live in `SUPPORT_MATRIX
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
65
|
-
|
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
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,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
|
-
"
|
|
6
|
+
"kimi-k2.7-code",
|
|
7
|
+
"kimi-k3",
|
|
8
|
+
"glm-5.2",
|
|
7
9
|
"deepseek-v4-pro",
|
|
8
|
-
"
|
|
9
|
-
"
|
|
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,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,
|
|
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,
|
|
1
|
+
export { createOpencodeZenPlugin, } from "./deployments/opencode-zen.js";
|
|
2
|
+
export { createOpenAIPlugin, } from "./deployments/openai.js";
|
|
3
|
+
export { createXaiPlugin } from "./deployments/xai.js";
|
package/dist/protocol/body.d.ts
CHANGED
package/dist/protocol/body.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 !==
|
|
179
|
-
throw new Error(
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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.
|
|
15
|
+
"@intx/inference-discovery": "0.3.0",
|
|
15
16
|
"arktype": "^2.1.29"
|
|
16
17
|
},
|
|
17
18
|
"files": [
|