@agenticprimitives/orchestration-openai-compat 0.0.0-alpha.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Agentic Trust Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # @agenticprimitives/orchestration-openai-compat
2
+
3
+ The **OpenAI-compatible LLM binding** for the [`@agenticprimitives/orchestration`](../orchestration) `Planner`
4
+ and `AnswerComposer` ports — the in-repo adapter (the `chain-state` / `chain-state-viem` pattern), sibling to
5
+ [`orchestration-anthropic`](../orchestration-anthropic). The pure orchestration core stays vendor-free; the
6
+ chat-completions translation lives here.
7
+
8
+ **Vendor-neutral.** This package names no host and no model. Groq, Ollama, OpenRouter, a self-hosted vLLM: each
9
+ is a `baseUrl` + `model` you supply.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pnpm add @agenticprimitives/orchestration-openai-compat @agenticprimitives/orchestration
15
+ ```
16
+
17
+ ## Quick start (Groq)
18
+
19
+ ```ts
20
+ import { runIntent, type ToolSpec } from '@agenticprimitives/orchestration';
21
+ import { createOpenAiCompatPlanner, createFetchOpenAiCompatClient } from '@agenticprimitives/orchestration-openai-compat';
22
+
23
+ const tools: ToolSpec[] = [
24
+ { id: 'get_vault_record', description: 'Read a vault record by recordType.', inputSchema: { type: 'object', properties: { recordType: { type: 'string' } }, required: ['recordType'] } },
25
+ { id: 'get_pii', description: 'Read the principal’s PII (high sensitivity).' },
26
+ ];
27
+
28
+ // Dependency-free client — works in a Cloudflare Worker (no SDK, no node shims). The host is YOUR choice.
29
+ const client = createFetchOpenAiCompatClient({ apiKey: env.GROQ_API_KEY, baseUrl: 'https://api.groq.com/openai/v1' });
30
+ const planner = createOpenAiCompatPlanner({ client, model: 'openai/gpt-oss-120b', label: 'groq' });
31
+
32
+ const result = await runIntent(
33
+ { goal: 'show me my profile' },
34
+ { planner, tools, invoke: async (toolId, args) => myDelegatedMcpCall(toolId, args) },
35
+ );
36
+ ```
37
+
38
+ The model chooses **which** tool fits the goal; the chosen call still rides the task's on-chain delegation at
39
+ the invoker (ADR-0041). The model never gains authority by being asked.
40
+
41
+ ## What else is here
42
+
43
+ - `createOpenAiCompatComposer({ client, model })` — turns what the tools returned into a grounded sentence.
44
+ - `createOpenAiCompatStructuredCall({ client, model })` — one forced single-tool call (a query, a selection).
45
+ - `OpenAiCompatHttpError` — thrown on a non-OK response with `status` and, on a 429, `retryAfterSeconds`. Say
46
+ it to the person; never route to another vendor because of it.
47
+
48
+ ## `OpenAiCompatLike`
49
+
50
+ Every factory takes a structural `OpenAiCompatLike` client (the minimal `chat.completions.create` surface).
51
+ Pass either `createFetchOpenAiCompatClient({ apiKey, baseUrl })` or the official `new OpenAI({ apiKey, baseURL })`
52
+ — it satisfies the same shape. This keeps the adapter unit-testable with a fake and avoids a hard SDK dependency.
53
+
54
+ ## Wire notes
55
+
56
+ - Tool `arguments` arrive as a JSON **string**; the adapter parses them. Unparseable → `planner_failed`.
57
+ - `tool_choice: 'required'` is a request; a prose-only reply is `no_plan`.
58
+ - The client is non-streaming. Fine for planning / answering budgets; a very long generation may hit an
59
+ intermediary timeout until streaming lands.
60
+
61
+ ## Boundaries
62
+
63
+ Imports only `@agenticprimitives/orchestration` (+ `types`). See `spec.md` + `capability.manifest.json`.
64
+
65
+ ## License
66
+
67
+ MIT
@@ -0,0 +1,16 @@
1
+ import type { AnswerComposer } from '@agenticprimitives/orchestration';
2
+ import type { OpenAiCompatLike } from './types.js';
3
+ export interface OpenAiCompatComposerOpts {
4
+ client: OpenAiCompatLike;
5
+ /** The answering model, as the host names it. REQUIRED. */
6
+ model: string;
7
+ maxTokens?: number;
8
+ /** Extra guidance appended after the contract above (e.g. an org's voice). Never replaces it. */
9
+ systemPrompt?: string;
10
+ /** Cap on the observation JSON handed to the model, in characters. */
11
+ maxEvidenceChars?: number;
12
+ /** Names the consumer in errors. Default `openai-compat`. */
13
+ label?: string;
14
+ }
15
+ export declare function createOpenAiCompatComposer(opts: OpenAiCompatComposerOpts): AnswerComposer;
16
+ //# sourceMappingURL=composer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composer.d.ts","sourceRoot":"","sources":["../src/composer.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,cAAc,EAAgB,MAAM,kCAAkC,CAAC;AAErF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAsBnD,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,gBAAgB,CAAC;IACzB,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iGAAiG;IACjG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAMD,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,wBAAwB,GAAG,cAAc,CAoCzF"}
@@ -0,0 +1,66 @@
1
+ import { fitEvidence } from '@agenticprimitives/orchestration';
2
+ const SYSTEM = [
3
+ 'You put an agent\'s findings into plain words for the person who asked.',
4
+ '',
5
+ 'You are given the question and the RESULTS the agent\'s tools returned. Answer ONLY from those results.',
6
+ 'Every name, number, address and relationship you state must appear in them.',
7
+ '',
8
+ 'If the results are empty or do not cover the question, say WHAT WAS LOOKED FOR and that it matched',
9
+ 'nothing — "nothing matched a name search for X" — and stop. Never restate an empty lookup as a fact',
10
+ 'about the world ("X does not exist"): you know how the search went, not what exists.',
11
+ 'And never fill a gap with something plausible: an invented agent is worse than no answer.',
12
+ '',
13
+ 'Be brief and concrete. Lead with the answer. Prefer a short list to a paragraph when the results are a',
14
+ 'list. Do not describe the tools you used, do not mention JSON or queries, and do not offer to do more.',
15
+ '',
16
+ 'ANSWER THE QUESTION THAT WAS ASKED, FIRST. If it asks for one fact (a balance, a count, a name), the',
17
+ 'first sentence is that fact, in the unit the results display (never a raw base-unit figure). If the',
18
+ 'results do not contain the asked fact, say so in one sentence and stop — do not summarise other data',
19
+ 'in its place, and do not compute what the results do not state.',
20
+ ].join('\n');
21
+ const stringify = (v) => {
22
+ try {
23
+ return JSON.stringify(v, (_k, x) => (typeof x === 'bigint' ? x.toString() : x));
24
+ }
25
+ catch {
26
+ return String(v);
27
+ }
28
+ };
29
+ export function createOpenAiCompatComposer(opts) {
30
+ const model = opts.model;
31
+ const label = opts.label ?? 'openai-compat';
32
+ if (!model)
33
+ throw new Error(`${label} composer: model is required — this adapter names no vendor default`);
34
+ const maxTokens = opts.maxTokens ?? 700;
35
+ const cap = opts.maxEvidenceChars ?? 24_000;
36
+ return {
37
+ evidenceBudget: cap,
38
+ async compose({ intent, observations, corrections, evidence }) {
39
+ // Only what SUCCEEDED is evidence (a failed step is not a fact about the world), FITTED to the budget —
40
+ // by the caller when it hands `evidence` (the drops are then on its trace), else here. Never sliced:
41
+ // a body that does not fit is replaced by its summary and said so (spec 391).
42
+ const items = evidence ?? fitEvidence(observations, cap).items;
43
+ const body = stringify(items);
44
+ const res = await opts.client.chat.completions.create({
45
+ model, max_tokens: maxTokens,
46
+ messages: [
47
+ { role: 'system', content: opts.systemPrompt ? `${SYSTEM}\n\n${opts.systemPrompt}` : SYSTEM },
48
+ {
49
+ role: 'user',
50
+ content: `Question: ${intent.goal}\n\nResults:\n${body}${corrections?.length
51
+ // The governor's own words, verbatim: it states what was claimed and what the evidence
52
+ // supports, which is exactly the instruction a recompose needs.
53
+ ? `\n\nYour previous answer overstated the results. Corrections that MUST be honoured:\n${corrections.map((c) => `- ${c}`).join('\n')}`
54
+ : ''}`,
55
+ },
56
+ ],
57
+ });
58
+ const text = (res.choices?.[0]?.message?.content ?? '').trim();
59
+ // No text is not an answer. Say nothing rather than something: the caller renders the raw result.
60
+ if (!text)
61
+ throw new Error('the composer returned no text');
62
+ return text;
63
+ },
64
+ };
65
+ }
66
+ //# sourceMappingURL=composer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composer.js","sourceRoot":"","sources":["../src/composer.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAG/D,MAAM,MAAM,GAAG;IACb,yEAAyE;IACzE,EAAE;IACF,yGAAyG;IACzG,6EAA6E;IAC7E,EAAE;IACF,oGAAoG;IACpG,qGAAqG;IACrG,sFAAsF;IACtF,2FAA2F;IAC3F,EAAE;IACF,wGAAwG;IACxG,wGAAwG;IACxG,EAAE;IACF,sGAAsG;IACtG,qGAAqG;IACrG,sGAAsG;IACtG,iEAAiE;CAClE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAeb,MAAM,SAAS,GAAG,CAAC,CAAU,EAAU,EAAE;IACvC,IAAI,CAAC;QAAC,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC;IAAC,CAAC;AACtH,CAAC,CAAC;AAEF,MAAM,UAAU,0BAA0B,CAAC,IAA8B;IACvE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;IACzB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,eAAe,CAAC;IAC5C,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,qEAAqE,CAAC,CAAC;IAC3G,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,GAAG,CAAC;IACxC,MAAM,GAAG,GAAG,IAAI,CAAC,gBAAgB,IAAI,MAAM,CAAC;IAC5C,OAAO;QACL,cAAc,EAAE,GAAG;QACnB,KAAK,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAgB;YACzE,wGAAwG;YACxG,qGAAqG;YACrG,8EAA8E;YAC9E,MAAM,KAAK,GAAG,QAAQ,IAAI,WAAW,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC;YAC/D,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;YAC9B,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC;gBACpD,KAAK,EAAE,UAAU,EAAE,SAAS;gBAC5B,QAAQ,EAAE;oBACR,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,MAAM,OAAO,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE;oBAC7F;wBACE,IAAI,EAAE,MAAM;wBACZ,OAAO,EAAE,aAAa,MAAM,CAAC,IAAI,iBAAiB,IAAI,GACpD,WAAW,EAAE,MAAM;4BACjB,uFAAuF;4BACvF,gEAAgE;4BAChE,CAAC,CAAC,wFAAwF,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;4BACvI,CAAC,CAAC,EACN,EAAE;qBACH;iBACF;aACF,CAAC,CAAC;YACH,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YAC/D,kGAAkG;YAClG,IAAI,CAAC,IAAI;gBAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;YAC5D,OAAO,IAAI,CAAC;QACd,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,42 @@
1
+ import type { OpenAiCompatLike } from './types.js';
2
+ export interface FetchOpenAiCompatOpts {
3
+ /** The API key. Never log this; the app reads it from a Worker secret. */
4
+ apiKey: string;
5
+ /** The OpenAI-compatible API base, e.g. `https://api.groq.com/openai/v1`. REQUIRED — this adapter names no vendor. */
6
+ baseUrl: string;
7
+ /** Injectable fetch (for tests / non-global-fetch runtimes). Default the global `fetch`. */
8
+ fetchImpl?: typeof fetch;
9
+ /** Send the SAME request once more when a reasoning model answered `tool_use_failed` (it reasoned and called no tool). Default true. */
10
+ retryOnNoToolCall?: boolean;
11
+ /** Extra request headers (a host's project or organization header, say). Never the auth header. */
12
+ extraHeaders?: Record<string, string>;
13
+ /**
14
+ * WHICH NAME THIS HOST WANTS FOR THE COMPLETION BOUND. `max_tokens` is what most compatible hosts accept
15
+ * and stays the default; OpenAI's own reasoning families REFUSE it ("Unsupported parameter: 'max_tokens'
16
+ * is not supported with this model") rather than ignoring it, and want `max_completion_tokens`. The
17
+ * consumer names the host, so the consumer names the field — this package still names no vendor. The
18
+ * client sends exactly one of the two, never both.
19
+ */
20
+ tokenLimitParam?: 'max_tokens' | 'max_completion_tokens';
21
+ /**
22
+ * On a 429 whose `retry-after` is at most this many seconds, wait that long and send the SAME request once
23
+ * more. A metered host (Groq's free plan: tokens per minute) answers the second ask in a minute with "try
24
+ * again in 6s", and failing the whole turn for six seconds is worse than a six-second turn. This is the
25
+ * bounded retry of one call ADR-0013 permits — never a different request, never another vendor — and a
26
+ * longer wait, or a second 429, surfaces as the error it is. `0` disables it. Default 30.
27
+ */
28
+ waitOn429UpToSeconds?: number;
29
+ /** Injectable sleep (tests). */
30
+ sleep?: (ms: number) => Promise<void>;
31
+ }
32
+ /** A non-OK HTTP response, with what a caller needs to SAY about it: the status and, on a 429, how long the
33
+ * host asked us to wait. Surfaces as-is — never a cue to route to another vendor (ADR-0013). */
34
+ export declare class OpenAiCompatHttpError extends Error {
35
+ readonly status: number;
36
+ readonly retryAfterSeconds: number | undefined;
37
+ readonly detail: string;
38
+ constructor(status: number, detail: string, retryAfterSeconds?: number);
39
+ }
40
+ /** Build a minimal `OpenAiCompatLike` client backed by `fetch`. */
41
+ export declare function createFetchOpenAiCompatClient(opts: FetchOpenAiCompatOpts): OpenAiCompatLike;
42
+ //# sourceMappingURL=fetch-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-client.d.ts","sourceRoot":"","sources":["../src/fetch-client.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,gBAAgB,EAA4D,MAAM,YAAY,CAAC;AAE7G,MAAM,WAAW,qBAAqB;IACpC,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf,sHAAsH;IACtH,OAAO,EAAE,MAAM,CAAC;IAChB,4FAA4F;IAC5F,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,wIAAwI;IACxI,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,mGAAmG;IACnG,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,YAAY,GAAG,uBAAuB,CAAC;IACzD;;;;;;OAMG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,gCAAgC;IAChC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAED;iGACiG;AACjG,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,iBAAiB,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBACZ,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,CAAC,EAAE,MAAM;CASvE;AAWD,mEAAmE;AACnE,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,qBAAqB,GAAG,gBAAgB,CAkD3F"}
@@ -0,0 +1,79 @@
1
+ /** A non-OK HTTP response, with what a caller needs to SAY about it: the status and, on a 429, how long the
2
+ * host asked us to wait. Surfaces as-is — never a cue to route to another vendor (ADR-0013). */
3
+ export class OpenAiCompatHttpError extends Error {
4
+ status;
5
+ retryAfterSeconds;
6
+ detail;
7
+ constructor(status, detail, retryAfterSeconds) {
8
+ super(`openai-compat chat.completions.create failed (HTTP ${status}${retryAfterSeconds !== undefined ? `, retry after ${retryAfterSeconds}s` : ''})${detail ? `: ${detail}` : ''}`);
9
+ this.name = 'OpenAiCompatHttpError';
10
+ this.status = status;
11
+ this.retryAfterSeconds = retryAfterSeconds;
12
+ this.detail = detail;
13
+ }
14
+ }
15
+ function retryAfterOf(resp) {
16
+ const raw = resp.headers.get('retry-after');
17
+ if (!raw)
18
+ return undefined;
19
+ const n = Number(raw);
20
+ if (Number.isFinite(n) && n >= 0)
21
+ return n;
22
+ const at = Date.parse(raw);
23
+ return Number.isFinite(at) ? Math.max(0, Math.round((at - Date.now()) / 1000)) : undefined;
24
+ }
25
+ /** Build a minimal `OpenAiCompatLike` client backed by `fetch`. */
26
+ export function createFetchOpenAiCompatClient(opts) {
27
+ const base = (opts.baseUrl ?? '').trim().replace(/\/+$/, '');
28
+ if (!base)
29
+ throw new Error('createFetchOpenAiCompatClient: baseUrl is required — this adapter names no vendor; the consumer supplies the host');
30
+ const f = opts.fetchImpl ?? fetch;
31
+ return {
32
+ chat: {
33
+ completions: {
34
+ async create(params) {
35
+ // One bound, under the name this host wants (see `tokenLimitParam`).
36
+ const body = opts.tokenLimitParam === 'max_completion_tokens'
37
+ ? { ...params, max_completion_tokens: params.max_tokens, max_tokens: undefined }
38
+ : params;
39
+ const send = () => f(`${base}/chat/completions`, {
40
+ method: 'POST',
41
+ headers: {
42
+ 'content-type': 'application/json',
43
+ authorization: `Bearer ${opts.apiKey}`,
44
+ ...(opts.extraHeaders ?? {}),
45
+ },
46
+ body: JSON.stringify(body),
47
+ });
48
+ const waitUpTo = opts.waitOn429UpToSeconds ?? 30;
49
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
50
+ let resp = await send();
51
+ if (resp.status === 429 && waitUpTo > 0) {
52
+ const after = retryAfterOf(resp);
53
+ if (after !== undefined && after <= waitUpTo) {
54
+ await resp.text().catch(() => '');
55
+ await sleep(Math.ceil(after * 1000) + 250);
56
+ resp = await send();
57
+ }
58
+ }
59
+ if (!resp.ok) {
60
+ let detail = await resp.text().catch(() => '');
61
+ // A REASONING MODEL THAT REASONED AND CALLED NO TOOL (Groq's gpt-oss under `tool_choice: 'required'`
62
+ // answers HTTP 400 `tool_use_failed`, seen live 2026-09-10 on a request identical to one that had
63
+ // planned fine minutes before). The same request once more is the one bounded retry ADR-0013 permits:
64
+ // same body, same host, no different prompt and no other vendor. A second refusal is the answer.
65
+ if (resp.status === 400 && /"code"\s*:\s*"tool_use_failed"/.test(detail) && (opts.retryOnNoToolCall ?? true)) {
66
+ resp = await send();
67
+ if (resp.ok)
68
+ return (await resp.json());
69
+ detail = await resp.text().catch(() => '');
70
+ }
71
+ throw new OpenAiCompatHttpError(resp.status, detail.slice(0, 2_000), retryAfterOf(resp));
72
+ }
73
+ return (await resp.json());
74
+ },
75
+ },
76
+ },
77
+ };
78
+ }
79
+ //# sourceMappingURL=fetch-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-client.js","sourceRoot":"","sources":["../src/fetch-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ export { PACKAGE_NAME, PACKAGE_STATUS, SPEC_REF } from './types.js';
2
+ export type { OpenAiCompatLike, OpenAiCompatChatCreateParams, OpenAiCompatChatCompletion, OpenAiCompatChatMessage, OpenAiCompatToolDef, OpenAiCompatToolChoice, OpenAiCompatToolCall, } from './types.js';
3
+ export { createOpenAiCompatPlanner, parseToolArguments } from './planner.js';
4
+ export type { OpenAiCompatPlannerOpts } from './planner.js';
5
+ export { createFetchOpenAiCompatClient, OpenAiCompatHttpError } from './fetch-client.js';
6
+ export type { FetchOpenAiCompatOpts } from './fetch-client.js';
7
+ export { createOpenAiCompatComposer } from './composer.js';
8
+ export type { OpenAiCompatComposerOpts } from './composer.js';
9
+ export { createOpenAiCompatStructuredCall } from './structured-call.js';
10
+ export type { OpenAiCompatStructuredCallOpts, StructuredToolCallInput } from './structured-call.js';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACpE,YAAY,EACV,gBAAgB,EAChB,4BAA4B,EAC5B,0BAA0B,EAC1B,uBAAuB,EACvB,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAC7E,YAAY,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAE5D,OAAO,EAAE,6BAA6B,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AACzF,YAAY,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAG/D,OAAO,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAC3D,YAAY,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAG9D,OAAO,EAAE,gCAAgC,EAAE,MAAM,sBAAsB,CAAC;AACxE,YAAY,EAAE,8BAA8B,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ // @agenticprimitives/orchestration-openai-compat — public API. The OpenAI-compatible LLM binding for the
2
+ // orchestration `Planner` / `AnswerComposer` ports (the in-repo adapter, chain-state-viem pattern; spec 377).
3
+ // Vendor-neutral: the consumer supplies `baseUrl` + `model` (Groq, Ollama, OpenRouter, …). Authority is never
4
+ // here — a chosen tool's call still rides the task delegation at the invoker (ADR-0041).
5
+ export { PACKAGE_NAME, PACKAGE_STATUS, SPEC_REF } from './types.js';
6
+ export { createOpenAiCompatPlanner, parseToolArguments } from './planner.js';
7
+ export { createFetchOpenAiCompatClient, OpenAiCompatHttpError } from './fetch-client.js';
8
+ // Spec 350 §3.7 — the answering half: what the tools returned, in plain words, grounded in them.
9
+ export { createOpenAiCompatComposer } from './composer.js';
10
+ // Spec 358 W1 — the one forced single-tool call the semantic context plane makes.
11
+ export { createOpenAiCompatStructuredCall } from './structured-call.js';
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,yGAAyG;AACzG,8GAA8G;AAC9G,8GAA8G;AAC9G,yFAAyF;AAEzF,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAWpE,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAG7E,OAAO,EAAE,6BAA6B,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAGzF,iGAAiG;AACjG,OAAO,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAG3D,kFAAkF;AAClF,OAAO,EAAE,gCAAgC,EAAE,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { type Planner } from '@agenticprimitives/orchestration';
2
+ import type { OpenAiCompatLike } from './types.js';
3
+ export interface OpenAiCompatPlannerOpts {
4
+ /** The structural client (real `openai` SDK client OR `createFetchOpenAiCompatClient`). */
5
+ client: OpenAiCompatLike;
6
+ /** Planning model, as the host names it (`llama-3.3-70b-versatile`, `openai/gpt-oss-120b`, …). REQUIRED. */
7
+ model: string;
8
+ /** Max tokens for the planning turn. Default 1024. */
9
+ maxTokens?: number;
10
+ /** Reasoning effort for a reasoning model (`reasoning_effort` on the wire). Planning is one tool choice:
11
+ * live, gpt-oss-120b thought for 30s at the host's default and then called NO tool ("tool_use_failed",
12
+ * an empty generation) — the thinking had consumed the completion. `low` is the planner's setting. */
13
+ reasoningEffort?: 'minimal' | 'low' | 'medium' | 'high';
14
+ /** Override the system prompt (the default constrains the model to the tool-selection job). */
15
+ systemPrompt?: string;
16
+ /** Names the consumer in rationales and errors — `groq`, `ollama`. Default `openai-compat`. */
17
+ label?: string;
18
+ }
19
+ /**
20
+ * Parse a tool call's `arguments`. On this wire they are a JSON STRING, and a string that does not parse to an
21
+ * object is not "no arguments" — it is a generation that went wrong, and it fails loudly (`planner_failed`)
22
+ * rather than yielding a plausible step with empty args. Empty / whitespace IS no arguments.
23
+ */
24
+ export declare function parseToolArguments(raw: string | undefined, where: string): Record<string, unknown>;
25
+ /**
26
+ * Create a `Planner` backed by chat-completions tool calling. Fail-closed: if the model returns no tool call
27
+ * (prose only, despite `tool_choice: 'required'`), the planner throws `OrchestrationError('no_plan')` — the
28
+ * loop surfaces it as a failed run. One or more tool calls become the plan steps (in order). The loop's
29
+ * `assertKnownTools` is the backstop if the model ever names a tool outside the provided set.
30
+ */
31
+ export declare function createOpenAiCompatPlanner(opts: OpenAiCompatPlannerOpts): Planner;
32
+ //# sourceMappingURL=planner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"planner.d.ts","sourceRoot":"","sources":["../src/planner.ts"],"names":[],"mappings":"AAIA,OAAO,EAIL,KAAK,OAAO,EAMb,MAAM,kCAAkC,CAAC;AAC1C,OAAO,KAAK,EAAE,gBAAgB,EAAuB,MAAM,YAAY,CAAC;AAExE,MAAM,WAAW,uBAAuB;IACtC,2FAA2F;IAC3F,MAAM,EAAE,gBAAgB,CAAC;IACzB,4GAA4G;IAC5G,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;2GAEuG;IACvG,eAAe,CAAC,EAAE,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACxD,+FAA+F;IAC/F,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+FAA+F;IAC/F,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA+BD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAalG;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAkChF"}
@@ -0,0 +1,94 @@
1
+ // createOpenAiCompatPlanner — implements the orchestration `Planner` port via chat-completions tool calling.
2
+ // Present the goal + the agent's exposed tools to the model; read back the chosen tool + args as a `Plan`.
3
+ // Authority is NOT here — the chosen tool's call still rides the task delegation at the invoker (ADR-0041).
4
+ // The model only decides WHICH tool; it never gains authority by being asked.
5
+ import { OrchestrationError, toolNameMap, planStepFromToolArgs, } from '@agenticprimitives/orchestration';
6
+ const DEFAULT_SYSTEM = 'You are the planning component of an agent. Choose the single tool that best satisfies the user\'s goal, ' +
7
+ 'and provide its arguments. You may ONLY use the provided tools. Do not answer in prose — select a tool.';
8
+ /** Map an orchestration `ToolSpec` to a chat-completions tool definition (object-root schema by default). */
9
+ function toWireTool(t, wireName) {
10
+ return {
11
+ type: 'function',
12
+ function: {
13
+ name: wireName,
14
+ description: t.description,
15
+ parameters: t.inputSchema ?? { type: 'object', properties: {}, additionalProperties: true },
16
+ },
17
+ };
18
+ }
19
+ /** Build the user turn: the goal, plus any prior failed observations so a re-plan can choose differently. */
20
+ function buildUserMessage(input) {
21
+ const lines = [`Goal: ${input.intent.goal}`];
22
+ if (input.intent.constraints)
23
+ lines.push(`Constraints: ${JSON.stringify(input.intent.constraints)}`);
24
+ if (input.intent.context)
25
+ lines.push(`Context: ${JSON.stringify(input.intent.context)}`);
26
+ const failed = (input.observations ?? []).filter((o) => !o.ok);
27
+ if (failed.length) {
28
+ lines.push('Previous attempts failed — choose a different approach:');
29
+ for (const o of failed)
30
+ lines.push(`- ${o.step.toolId} failed: ${o.error ?? 'unknown error'}`);
31
+ }
32
+ return lines.join('\n');
33
+ }
34
+ /**
35
+ * Parse a tool call's `arguments`. On this wire they are a JSON STRING, and a string that does not parse to an
36
+ * object is not "no arguments" — it is a generation that went wrong, and it fails loudly (`planner_failed`)
37
+ * rather than yielding a plausible step with empty args. Empty / whitespace IS no arguments.
38
+ */
39
+ export function parseToolArguments(raw, where) {
40
+ const s = (raw ?? '').trim();
41
+ if (!s)
42
+ return {};
43
+ let parsed;
44
+ try {
45
+ parsed = JSON.parse(s);
46
+ }
47
+ catch (e) {
48
+ throw new OrchestrationError('planner_failed', `${where} returned tool arguments that are not valid JSON (${e instanceof Error ? e.message : String(e)})`);
49
+ }
50
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
51
+ throw new OrchestrationError('planner_failed', `${where} returned tool arguments that are not a JSON object`);
52
+ }
53
+ return parsed;
54
+ }
55
+ /**
56
+ * Create a `Planner` backed by chat-completions tool calling. Fail-closed: if the model returns no tool call
57
+ * (prose only, despite `tool_choice: 'required'`), the planner throws `OrchestrationError('no_plan')` — the
58
+ * loop surfaces it as a failed run. One or more tool calls become the plan steps (in order). The loop's
59
+ * `assertKnownTools` is the backstop if the model ever names a tool outside the provided set.
60
+ */
61
+ export function createOpenAiCompatPlanner(opts) {
62
+ const model = opts.model;
63
+ const label = opts.label ?? 'openai-compat';
64
+ if (!model)
65
+ throw new Error(`${label} planner: model is required — this adapter names no vendor default`);
66
+ return {
67
+ async plan(input) {
68
+ const names = toolNameMap(input.tools);
69
+ const res = await opts.client.chat.completions.create({
70
+ model,
71
+ max_tokens: opts.maxTokens ?? 1024,
72
+ messages: [
73
+ { role: 'system', content: opts.systemPrompt ?? DEFAULT_SYSTEM },
74
+ { role: 'user', content: buildUserMessage(input) },
75
+ ],
76
+ tools: input.tools.map((t) => toWireTool(t, names.toWire.get(t.id))),
77
+ tool_choice: 'required', // force a tool selection (no prose-only answer) — a request, not a guarantee
78
+ ...(opts.reasoningEffort ? { reasoning_effort: opts.reasoningEffort } : {}),
79
+ });
80
+ const calls = res.choices?.[0]?.message?.tool_calls ?? [];
81
+ const steps = calls
82
+ .filter((c) => c && c.type === 'function' && typeof c.function?.name === 'string')
83
+ // Map the API name BACK to the tool id. A name we did not issue is a hallucinated tool: kept as-is so
84
+ // the loop's unknown-tool gate refuses it (fail-closed), never mapped to a nearest id. The plan-shape
85
+ // convention (`$ref`, `$forEach`, `$after`, `$when`) rides inside the args; the core lifts it.
86
+ .map((c) => planStepFromToolArgs(names.fromWire.get(c.function.name) ?? c.function.name, parseToolArguments(c.function.arguments, `${label}(${model})`)));
87
+ if (steps.length === 0) {
88
+ throw new OrchestrationError('no_plan', `the model (${label}(${model})) selected no tool for the goal`);
89
+ }
90
+ return { steps, rationale: `${label}(${model})` };
91
+ },
92
+ };
93
+ }
94
+ //# sourceMappingURL=planner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"planner.js","sourceRoot":"","sources":["../src/planner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import type { OpenAiCompatLike } from './types.js';
2
+ export interface StructuredToolCallInput {
3
+ system: string;
4
+ messages: Array<{
5
+ role: 'user' | 'assistant';
6
+ content: string;
7
+ }>;
8
+ /** The one tool the model MUST call. `input_schema` is the Anthropic-shaped name the port uses; on this
9
+ * wire it becomes `function.parameters`. */
10
+ tool: {
11
+ name: string;
12
+ description: string;
13
+ input_schema: Record<string, unknown>;
14
+ };
15
+ maxTokens?: number;
16
+ }
17
+ export interface OpenAiCompatStructuredCallOpts {
18
+ client: OpenAiCompatLike;
19
+ /** The model, as the host names it. REQUIRED. */
20
+ model: string;
21
+ /** Names the consumer in errors. Default `openai-compat`. */
22
+ label?: string;
23
+ /** Reasoning effort for a reasoning model. The bound below covers reasoning AND the tool call together,
24
+ * so a host default of `medium` can spend the whole bound thinking and call nothing — which surfaces as
25
+ * "answered in prose", a confusing way to say "ran out of room". Omitted ⇒ the host's default. */
26
+ reasoningEffort?: 'minimal' | 'low' | 'medium' | 'high';
27
+ /** The completion bound when a call names none. Default 1500. */
28
+ maxTokens?: number;
29
+ }
30
+ /**
31
+ * Build the forced call. STRICTER than a permissive reading on purpose: a response with no tool call, or a
32
+ * call to a different tool, is an error — the caller offered exactly one tool and asked for exactly that one,
33
+ * so anything else is the model declining, and a declined structured call must not read as `{}` (an empty
34
+ * query, an empty selection) downstream.
35
+ */
36
+ export declare function createOpenAiCompatStructuredCall(opts: OpenAiCompatStructuredCallOpts): (input: StructuredToolCallInput) => Promise<Record<string, unknown>>;
37
+ //# sourceMappingURL=structured-call.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structured-call.d.ts","sourceRoot":"","sources":["../src/structured-call.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAGnD,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjE;iDAC6C;IAC7C,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC;IACnF,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,8BAA8B;IAC7C,MAAM,EAAE,gBAAgB,CAAC;IACzB,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;uGAEmG;IACnG,eAAe,CAAC,EAAE,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACxD,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAC9C,IAAI,EAAE,8BAA8B,GACnC,CAAC,KAAK,EAAE,uBAAuB,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAqBtE"}
@@ -0,0 +1,43 @@
1
+ // THE FORCED SINGLE-TOOL CALL — the one structured model call the semantic context plane makes (spec 358 W1:
2
+ // a SPARQL query over a projected schema, a record selection over a vault's inventory). System + question
3
+ // in, ONE tool's arguments out, and the arguments are the entire result.
4
+ //
5
+ // NOT A PLANNER. This never chooses tools, never sees the run, never loops. It is a grounded translator: the
6
+ // caller supplies everything the model may know, and validation of what comes back happens at the endpoint
7
+ // that executes it — never here.
8
+ //
9
+ // The return type is STRUCTURALLY the `StructuredCall` port in `@agenticprimitives/context`; this package
10
+ // does not import that package (an adapter depends on the core it binds, nothing else).
11
+ import { OrchestrationError, wireNameFor } from '@agenticprimitives/orchestration';
12
+ import { parseToolArguments } from './planner.js';
13
+ /**
14
+ * Build the forced call. STRICTER than a permissive reading on purpose: a response with no tool call, or a
15
+ * call to a different tool, is an error — the caller offered exactly one tool and asked for exactly that one,
16
+ * so anything else is the model declining, and a declined structured call must not read as `{}` (an empty
17
+ * query, an empty selection) downstream.
18
+ */
19
+ export function createOpenAiCompatStructuredCall(opts) {
20
+ const model = opts.model;
21
+ const label = opts.label ?? 'openai-compat';
22
+ if (!model)
23
+ throw new Error(`${label} structured call: model is required — this adapter names no vendor default`);
24
+ return async ({ system, messages, tool, maxTokens }) => {
25
+ const wire = wireNameFor(tool.name);
26
+ const res = await opts.client.chat.completions.create({
27
+ model,
28
+ max_tokens: maxTokens ?? opts.maxTokens ?? 1500,
29
+ messages: [{ role: 'system', content: system }, ...messages],
30
+ tools: [{ type: 'function', function: { name: wire, description: tool.description, parameters: tool.input_schema } }],
31
+ tool_choice: { type: 'function', function: { name: wire } },
32
+ ...(opts.reasoningEffort ? { reasoning_effort: opts.reasoningEffort } : {}),
33
+ });
34
+ const call = (res.choices?.[0]?.message?.tool_calls ?? []).find((c) => c && c.type === 'function');
35
+ if (!call)
36
+ throw new OrchestrationError('planner_failed', `${label}(${model}) answered in prose instead of calling "${tool.name}"`);
37
+ if (call.function.name !== wire) {
38
+ throw new OrchestrationError('planner_failed', `${label}(${model}) called "${call.function.name}" when only "${tool.name}" was offered`);
39
+ }
40
+ return parseToolArguments(call.function.arguments, `${label}(${model})`);
41
+ };
42
+ }
43
+ //# sourceMappingURL=structured-call.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structured-call.js","sourceRoot":"","sources":["../src/structured-call.ts"],"names":[],"mappings":"AAAA,6GAA6G;AAC7G,0GAA0G;AAC1G,yEAAyE;AACzE,EAAE;AACF,6GAA6G;AAC7G,2GAA2G;AAC3G,iCAAiC;AACjC,EAAE;AACF,0GAA0G;AAC1G,wFAAwF;AACxF,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAEnF,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAyBlD;;;;;GAKG;AACH,MAAM,UAAU,gCAAgC,CAC9C,IAAoC;IAEpC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;IACzB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,eAAe,CAAC;IAC5C,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,4EAA4E,CAAC,CAAC;IAClH,OAAO,KAAK,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE;QACrD,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC;YACpD,KAAK;YACL,UAAU,EAAE,SAAS,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI;YAC/C,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,GAAG,QAAQ,CAAC;YAC5D,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,UAAU,EAAE,IAAI,CAAC,YAAY,EAAE,EAAE,CAAC;YACrH,WAAW,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;YAC3D,GAAG,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,gBAAgB,EAAE,IAAI,CAAC,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC5E,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,UAAU,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC;QACnG,IAAI,CAAC,IAAI;YAAE,MAAM,IAAI,kBAAkB,CAAC,gBAAgB,EAAE,GAAG,KAAK,IAAI,KAAK,2CAA2C,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;QACpI,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;YAChC,MAAM,IAAI,kBAAkB,CAAC,gBAAgB,EAAE,GAAG,KAAK,IAAI,KAAK,aAAa,IAAI,CAAC,QAAQ,CAAC,IAAI,gBAAgB,IAAI,CAAC,IAAI,eAAe,CAAC,CAAC;QAC3I,CAAC;QACD,OAAO,kBAAkB,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,GAAG,KAAK,IAAI,KAAK,GAAG,CAAC,CAAC;IAC3E,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,77 @@
1
+ export declare const PACKAGE_NAME: "@agenticprimitives/orchestration-openai-compat";
2
+ export declare const PACKAGE_STATUS: "w1-adapter";
3
+ export declare const SPEC_REF: "packages/orchestration-openai-compat/spec.md";
4
+ /** One tool definition on the chat-completions wire: the planner maps each `ToolSpec` to this. */
5
+ export interface OpenAiCompatToolDef {
6
+ type: 'function';
7
+ function: {
8
+ name: string;
9
+ description?: string;
10
+ /** JSON Schema for the tool arguments (object root). */
11
+ parameters: Record<string, unknown>;
12
+ };
13
+ }
14
+ /** The `tool_choice` modes we use. `'required'` forces a call (the planner); the object form forces ONE tool
15
+ * (the structured call). A request, not a guarantee — `no_plan` stays the backstop. */
16
+ export type OpenAiCompatToolChoice = 'none' | 'auto' | 'required' | {
17
+ type: 'function';
18
+ function: {
19
+ name: string;
20
+ };
21
+ };
22
+ export interface OpenAiCompatChatMessage {
23
+ role: 'system' | 'user' | 'assistant';
24
+ content: string;
25
+ }
26
+ /** The chat-completions request subset we send. `max_tokens` is the widely-accepted name; hosts that prefer
27
+ * `max_completion_tokens` accept it as the deprecated alias. */
28
+ export interface OpenAiCompatChatCreateParams {
29
+ model: string;
30
+ max_tokens: number;
31
+ /** The same bound under the name the newer OpenAI models REQUIRE (`max_tokens` is refused outright by the
32
+ * reasoning families, not ignored). The adapter never sets both: a consumer picks the name its host wants
33
+ * with `tokenLimitParam`, and the client renames the one field on the way out. */
34
+ max_completion_tokens?: number;
35
+ messages: OpenAiCompatChatMessage[];
36
+ tools?: OpenAiCompatToolDef[];
37
+ tool_choice?: OpenAiCompatToolChoice;
38
+ temperature?: number;
39
+ /** OpenAI's reasoning knob, honoured by the reasoning models on compatible hosts (Groq's gpt-oss, qwen). A
40
+ * reasoning model spends COMPLETION tokens thinking before it calls a tool; for a turn whose whole job is
41
+ * one tool choice, `low` keeps the thinking from eating the budget and the clock. Omitted ⇒ host default. */
42
+ reasoning_effort?: 'minimal' | 'low' | 'medium' | 'high';
43
+ }
44
+ /** One tool call in a response. NOTE: `arguments` is a JSON STRING on this wire — the adapter parses it. */
45
+ export interface OpenAiCompatToolCall {
46
+ id?: string;
47
+ type: 'function';
48
+ function: {
49
+ name: string;
50
+ arguments: string;
51
+ };
52
+ }
53
+ /** The chat-completions response subset we read. */
54
+ export interface OpenAiCompatChatCompletion {
55
+ choices: Array<{
56
+ message: {
57
+ role?: string;
58
+ content: string | null;
59
+ tool_calls?: OpenAiCompatToolCall[];
60
+ };
61
+ finish_reason?: string | null;
62
+ }>;
63
+ usage?: {
64
+ prompt_tokens?: number;
65
+ completion_tokens?: number;
66
+ total_tokens?: number;
67
+ };
68
+ }
69
+ /** The structural client the adapter needs — satisfied by `new OpenAI()` OR `createFetchOpenAiCompatClient`. */
70
+ export interface OpenAiCompatLike {
71
+ chat: {
72
+ completions: {
73
+ create(params: OpenAiCompatChatCreateParams): Promise<OpenAiCompatChatCompletion>;
74
+ };
75
+ };
76
+ }
77
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,YAAY,EAAG,gDAAyD,CAAC;AACtF,eAAO,MAAM,cAAc,EAAG,YAAqB,CAAC;AACpD,eAAO,MAAM,QAAQ,EAAG,8CAAuD,CAAC;AAEhF,kGAAkG;AAClG,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,wDAAwD;QACxD,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACrC,CAAC;CACH;AAED;wFACwF;AACxF,MAAM,MAAM,sBAAsB,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAErH,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;iEACiE;AACjE,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB;;uFAEmF;IACnF,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,EAAE,uBAAuB,EAAE,CAAC;IACpC,KAAK,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC9B,WAAW,CAAC,EAAE,sBAAsB,CAAC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;kHAE8G;IAC9G,gBAAgB,CAAC,EAAE,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;CAC1D;AAED,4GAA4G;AAC5G,MAAM,WAAW,oBAAoB;IACnC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/C;AAED,oDAAoD;AACpD,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,KAAK,CAAC;QACb,OAAO,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;YAAC,UAAU,CAAC,EAAE,oBAAoB,EAAE,CAAA;SAAE,CAAC;QACxF,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAC/B,CAAC,CAAC;IACH,KAAK,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACvF;AAED,gHAAgH;AAChH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE;QACJ,WAAW,EAAE;YACX,MAAM,CAAC,MAAM,EAAE,4BAA4B,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAAC;SACnF,CAAC;KACH,CAAC;CACH"}
package/dist/types.js ADDED
@@ -0,0 +1,14 @@
1
+ // @agenticprimitives/orchestration-openai-compat — the OpenAI-compatible LLM binding for the orchestration
2
+ // Planner / AnswerComposer ports (spec 377).
3
+ //
4
+ // This is the in-repo ADAPTER (the chain-state / chain-state-viem pattern), sibling to `orchestration-anthropic`:
5
+ // the pure `orchestration` core stays vendor-free; the chat-completions protocol translation lives HERE. It
6
+ // is VENDOR-NEUTRAL by construction — it names no host and no model. Groq, Ollama, OpenRouter, a self-hosted
7
+ // vLLM: each is a `baseUrl` + `model` the CONSUMER supplies. We define a STRUCTURAL `OpenAiCompatLike` client
8
+ // rather than depending on the `openai` SDK, so (a) the planner is unit-testable with a fake, (b) a consumer
9
+ // can pass the real SDK client (it satisfies the shape) OR our dependency-free fetch client, and (c) this
10
+ // package needs no heavy runtime dep. The SDK remains the reference, not a build dependency.
11
+ export const PACKAGE_NAME = '@agenticprimitives/orchestration-openai-compat';
12
+ export const PACKAGE_STATUS = 'w1-adapter';
13
+ export const SPEC_REF = 'packages/orchestration-openai-compat/spec.md';
14
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,2GAA2G;AAC3G,6CAA6C;AAC7C,EAAE;AACF,kHAAkH;AAClH,4GAA4G;AAC5G,6GAA6G;AAC7G,8GAA8G;AAC9G,6GAA6G;AAC7G,0GAA0G;AAC1G,6FAA6F;AAE7F,MAAM,CAAC,MAAM,YAAY,GAAG,gDAAyD,CAAC;AACtF,MAAM,CAAC,MAAM,cAAc,GAAG,YAAqB,CAAC;AACpD,MAAM,CAAC,MAAM,QAAQ,GAAG,8CAAuD,CAAC"}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@agenticprimitives/orchestration-openai-compat",
3
+ "version": "0.0.0-alpha.1",
4
+ "description": "OpenAI-compatible chat-completions binding for the @agenticprimitives/orchestration Planner / AnswerComposer ports (the in-repo adapter, chain-state-viem pattern). Vendor-neutral: the consumer supplies baseUrl + model (Groq, Ollama, OpenRouter, …). createOpenAiCompatPlanner turns a goal + exposed tools into a plan via tool calling, plus a composer, a forced single-tool structured call, and a dependency-free fetch client (Cloudflare-Worker friendly). The pure orchestration core stays vendor-free; authority is never here.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/agentictrustlabs/agenticprimitives.git",
9
+ "directory": "packages/orchestration-openai-compat"
10
+ },
11
+ "homepage": "https://github.com/agentictrustlabs/agenticprimitives/tree/master/packages/orchestration-openai-compat",
12
+ "bugs": {
13
+ "url": "https://github.com/agentictrustlabs/agenticprimitives/issues"
14
+ },
15
+ "type": "module",
16
+ "main": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ }
23
+ },
24
+ "files": [
25
+ "LICENSE",
26
+ "dist",
27
+ "spec.md",
28
+ "README.md"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "peerDependencies": {
34
+ "@agenticprimitives/orchestration": "0.0.0-alpha.1"
35
+ },
36
+ "devDependencies": {
37
+ "vitest": "^4.1.8",
38
+ "@agenticprimitives/orchestration": "0.0.0-alpha.1"
39
+ },
40
+ "keywords": [
41
+ "agentic",
42
+ "primitives",
43
+ "orchestration",
44
+ "planner",
45
+ "openai-compatible",
46
+ "groq",
47
+ "llm-adapter"
48
+ ],
49
+ "scripts": {
50
+ "build": "tsc -p tsconfig.build.json",
51
+ "typecheck": "tsc -p tsconfig.json --noEmit",
52
+ "test": "vitest run",
53
+ "test:unit": "vitest run test/unit --passWithNoTests",
54
+ "test:watch": "vitest",
55
+ "clean": "rm -rf dist"
56
+ }
57
+ }
package/spec.md ADDED
@@ -0,0 +1,66 @@
1
+ # orchestration-openai-compat — the OpenAI-compatible LLM binding
2
+
3
+ **Status:** w1-adapter. **Doctrine:** [ADR-0044](../../docs/architecture/decisions/0044-a2a-is-the-agentic-orchestration-layer.md)
4
+ + [spec 377](../../specs/377-a-second-model-behind-the-same-port.md). **Core:** [`@agenticprimitives/orchestration`](../orchestration/spec.md).
5
+ **Sibling:** [`orchestration-anthropic`](../orchestration-anthropic/spec.md).
6
+
7
+ ## 1. Why
8
+
9
+ The doctrine: orchestration is a Ring-0 primitive, but ONLY the concrete LLM-framework binding lives in a
10
+ dedicated in-repo adapter behind the port (the `chain-state` + `chain-state-viem` pattern — the core stays
11
+ pure; the vendor specifics are exiled to the adapter). This package is that adapter for every host that speaks
12
+ the OpenAI chat-completions wire. It names NO host and NO model: Groq, Ollama, OpenRouter, a self-hosted vLLM
13
+ are each a `baseUrl` + `model` the consumer supplies. That is the difference from the Anthropic sibling, which
14
+ binds one vendor and may default its model.
15
+
16
+ ## 2. The binding
17
+
18
+ - **`createOpenAiCompatPlanner({ client, model, maxTokens?, systemPrompt?, label? })`** implements the core's
19
+ `Planner` port. It maps each exposed `ToolSpec` to a `{ type: 'function', function: { name, description,
20
+ parameters } }` tool, sends a system message + the goal (plus any prior failed observations, so a re-plan
21
+ chooses differently) with `tool_choice: 'required'`, and turns `choices[0].message.tool_calls` into `Plan`
22
+ steps. No tool call → `OrchestrationError('no_plan')` (fail-closed; `required` is a request, not a guarantee).
23
+ - **Arguments are a JSON string** on this wire. Empty → `{}`. Unparseable or not an object →
24
+ `OrchestrationError('planner_failed')` — never a plausible step with silently empty args.
25
+ - **The plan-shape convention is the core's.** `toolNameMap` (dotted ids → API-safe names, collisions refused)
26
+ and `planStepFromToolArgs` (`$ref` / `$forEach` / `$after` / `$when` lifted from the args) are imported from
27
+ `@agenticprimitives/orchestration`; a hallucinated tool name is kept verbatim so the loop's `assertKnownTools`
28
+ refuses it. The model decides *which* tool; it never gains *authority*.
29
+ - **`createOpenAiCompatComposer({ client, model, … })`** implements `AnswerComposer` with the SAME grounding
30
+ contract as the Anthropic composer: only successful observations are evidence; empty text throws.
31
+ - **`createOpenAiCompatStructuredCall({ client, model, label? })`** is the forced single-tool call the semantic
32
+ context plane uses (spec 358 W1). Structurally the `StructuredCall` port in `@agenticprimitives/context`,
33
+ which this package does not import. Stricter than a permissive reading: no call, a different tool, or bad
34
+ JSON all throw — a declined structured call must not read as `{}` downstream.
35
+
36
+ ## 3. The structural client (`OpenAiCompatLike`)
37
+
38
+ We DEFINE the minimal `chat.completions.create` request/response shapes rather than importing `openai`:
39
+ the planner is unit-testable with a fake; a consumer may pass the real SDK client OR
40
+ `createFetchOpenAiCompatClient({ apiKey, baseUrl, fetchImpl?, extraHeaders? })` — dependency-free, Worker-safe,
41
+ POST `${baseUrl}/chat/completions` with `Authorization: Bearer`. `baseUrl` is REQUIRED. A non-OK response is
42
+ `OpenAiCompatHttpError { status, retryAfterSeconds?, detail }`, so a rate limit can be SAID, never rerouted.
43
+
44
+ Non-streaming. The Anthropic client streams because a long blocking generation was killed by an intermediary
45
+ at ~156s (HTTP 524); the turns this adapter serves first are 1024 / 700 tokens on a fast host. Streaming is a
46
+ follow-up, and a consumer running a 16k-token work turn through this client should expect that story to repeat
47
+ until it lands.
48
+
49
+ ## 4. Runtime activation
50
+
51
+ The app decides. demo-a2a (spec 377 §3) lists providers in `ORCHESTRATION_LLM` (`anthropic,groq`), reads
52
+ `GROQ_API_KEY` from a Worker secret and `ORCHESTRATION_GROQ_MODEL` / `ORCHESTRATION_GROQ_BASE_URL` with app-level
53
+ defaults, and constructs this adapter with `label: 'groq'`. A provider that is listed and not credentialed is a
54
+ thrown configuration error, never a fallback (ADR-0013).
55
+
56
+ ## 5. Boundaries
57
+
58
+ Imports `@agenticprimitives/orchestration` (+ `types`). Forbidden: `a2a`, `mcp-runtime`, `mcp-protocol`,
59
+ `delegation`, `service-agent`, `orchestration-anthropic`, `context`, `openai`, `@anthropic-ai/sdk`, `langchain`.
60
+ Authority, the loop, and the model live elsewhere.
61
+
62
+ ## 6. Out of scope
63
+
64
+ Streaming; multi-turn tool-result message threads (the loop re-plans with observation summaries); host-specific
65
+ extensions (`reasoning` fields, `response_format: json_schema`, `parallel_tool_calls`); any vendor's default
66
+ model or host.