@springbrand/model-client 0.0.0-stage → 0.1.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/README.md CHANGED
@@ -1,3 +1,78 @@
1
- # Temporary Holding Version
1
+ # @springbrand/model-client
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Native OpenRouter HTTP and payload helpers for Node and Workers. Runtime dependencies: none.
4
+ The package does not depend on Pi, an Agent runtime, environment variables or platform bindings.
5
+ Callers provide credentials, protocol-specific base URLs, model IDs and business schemas.
6
+
7
+ ```ts
8
+ import {
9
+ requestOpenRouter,
10
+ withOpenRouterRouting,
11
+ withOpenRouterStructuredOutput,
12
+ } from "@springbrand/model-client";
13
+
14
+ const protocol = "openrouter-chat";
15
+ const body = withOpenRouterRouting(withOpenRouterStructuredOutput({
16
+ model: modelId,
17
+ messages: [{ role: "user", content: prompt }],
18
+ stream: false,
19
+ }, protocol, schema));
20
+ const response = await requestOpenRouter({
21
+ protocol, apiKey, baseURL, headers: gatewayHeaders,
22
+ }, body, { signal });
23
+ ```
24
+
25
+ `requestOpenRouter` preserves the native body and returns the original `Response`,
26
+ including non-2xx status, request IDs, usage and SSE. Callers check status and
27
+ validate generated content against their schema. No retries or response parsing
28
+ are added. Payload helpers are opt-in; raw requests remain lossless.
29
+
30
+ Base URLs follow the native protocol. Supply resolved Gateway URLs and headers
31
+ in the same way; the client never rewrites one version prefix into another.
32
+
33
+ | Protocol | Direct base URL | Appended path |
34
+ | --- | --- | --- |
35
+ | `openrouter-messages` | `https://openrouter.ai/api` | `/v1/messages` |
36
+ | `openrouter-chat` | `https://openrouter.ai/api/v1` | `/chat/completions` |
37
+ | `openrouter-responses` | `https://openrouter.ai/api/v1` | `/responses` |
38
+ | `openrouter-decisions` | `https://openrouter.ai/api/alpha` | `/decisions` |
39
+
40
+ Jev uses OpenRouter's native [Decisions API](https://openrouter.ai/docs/api/api-reference/alphadecisions/submit-a-decisions-request)
41
+ with the same OpenRouter key and transport:
42
+
43
+ ```ts
44
+ const response = await requestOpenRouter({
45
+ protocol: "openrouter-decisions",
46
+ baseURL: "https://openrouter.ai/api/alpha",
47
+ apiKey,
48
+ }, {
49
+ model: decisionModelId,
50
+ state: { task: "Find a source-control connector" },
51
+ questions: {
52
+ source: {
53
+ type: "choice",
54
+ instructions: "Which source fits the task?",
55
+ criteria: { git: "Source control", calendar: "Scheduling" },
56
+ },
57
+ },
58
+ }, { signal });
59
+ ```
60
+
61
+ The body and response stay native: `state`, `questions`, `answers`, probabilities,
62
+ usage and future fields are untouched. Choice, Score, Noul and Box questions
63
+ follow the [Jev model's capabilities](https://openrouter.ai/docs/guides/community/jev).
64
+ Callers choose the model and validate answers, then apply their own ranking,
65
+ thresholds and follow-up policy. `withOpenRouterStructuredOutput` accepts only
66
+ `OpenRouterGenerationProtocol`; Decisions supplies its own typed questions.
67
+
68
+ Pi consumers inject `createOpenRouterFetch({ apiKey, fetch })` into Pi's `fetch`
69
+ option. Pi retains model registration, message conversion, Tool execution and
70
+ stream parsing. `openRouterHeaders` also prepares Bearer headers for SDKs that
71
+ validate credentials before invoking fetch. A configured key overrides incoming
72
+ Authorization; `x-api-key` is removed. Fetch without a configured key preserves
73
+ an SDK-supplied Authorization header.
74
+
75
+ Source, declarations and runtime JavaScript are built with `pnpm build`;
76
+ `pnpm pack` runs the same build. The published package exports only compiled ESM
77
+ and declarations. Source tests run in Node and Workers through the repository's
78
+ Vitest configuration.
@@ -0,0 +1,21 @@
1
+ /** Native APIs that generate assistant responses. */
2
+ export type OpenRouterGenerationProtocol = "openrouter-messages" | "openrouter-chat" | "openrouter-responses";
3
+ export type OpenRouterProtocol = OpenRouterGenerationProtocol | "openrouter-decisions";
4
+ /** A caller-owned endpoint with resolved credentials. */
5
+ export interface OpenRouterEndpoint {
6
+ readonly protocol: OpenRouterProtocol;
7
+ readonly baseURL: string;
8
+ readonly apiKey: string;
9
+ readonly headers?: Readonly<Record<string, string>>;
10
+ }
11
+ export interface OpenRouterRequestOptions {
12
+ readonly headers?: HeadersInit;
13
+ readonly signal?: AbortSignal;
14
+ readonly fetch?: typeof globalThis.fetch;
15
+ }
16
+ /** Credentials may also be supplied by an SDK in the outgoing Request. */
17
+ export interface OpenRouterFetchOptions {
18
+ readonly apiKey?: string;
19
+ readonly headers?: HeadersInit;
20
+ readonly fetch?: typeof globalThis.fetch;
21
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { OpenRouterEndpoint } from "./contracts.js";
2
+ /** Caller supplies the native protocol base (/api, /api/v1, or /api/alpha). */
3
+ export declare function openRouterRequestUrl(endpoint: Pick<OpenRouterEndpoint, "protocol" | "baseURL">): string;
@@ -0,0 +1,11 @@
1
+ /** Caller supplies the native protocol base (/api, /api/v1, or /api/alpha). */
2
+ export function openRouterRequestUrl(endpoint) {
3
+ const baseURL = endpoint.baseURL.replace(/\/+$/, "");
4
+ switch (endpoint.protocol) {
5
+ case "openrouter-messages": return `${baseURL}/v1/messages`;
6
+ case "openrouter-chat": return `${baseURL}/chat/completions`;
7
+ case "openrouter-responses": return `${baseURL}/responses`;
8
+ case "openrouter-decisions": return `${baseURL}/decisions`;
9
+ default: throw new Error(`Not an OpenRouter protocol: ${endpoint.protocol}`);
10
+ }
11
+ }
@@ -0,0 +1,5 @@
1
+ export type { OpenRouterEndpoint, OpenRouterFetchOptions, OpenRouterGenerationProtocol, OpenRouterProtocol, OpenRouterRequestOptions, } from "./contracts.js";
2
+ export { openRouterRequestUrl } from "./endpoint.js";
3
+ export { createOpenRouterFetch, openRouterHeaders, requestOpenRouter } from "./transport.js";
4
+ export { withOpenRouterRouting } from "./routing.js";
5
+ export { withOpenRouterStructuredOutput } from "./structured-output.js";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { openRouterRequestUrl } from "./endpoint.js";
2
+ export { createOpenRouterFetch, openRouterHeaders, requestOpenRouter } from "./transport.js";
3
+ export { withOpenRouterRouting } from "./routing.js";
4
+ export { withOpenRouterStructuredOutput } from "./structured-output.js";
@@ -0,0 +1,2 @@
1
+ /** Apply native OpenRouter routing without owning model or provider policy. */
2
+ export declare function withOpenRouterRouting(body: Readonly<Record<string, unknown>>, routing?: Readonly<Record<string, unknown>>, sessionId?: string): Record<string, unknown>;
@@ -0,0 +1,13 @@
1
+ /** Apply native OpenRouter routing without owning model or provider policy. */
2
+ export function withOpenRouterRouting(body, routing, sessionId) {
3
+ const provider = body.provider;
4
+ return {
5
+ ...body,
6
+ ...(sessionId ? { session_id: sessionId } : {}),
7
+ provider: {
8
+ ...(provider && typeof provider === "object" && !Array.isArray(provider) ? provider : {}),
9
+ ...routing,
10
+ require_parameters: true,
11
+ },
12
+ };
13
+ }
@@ -0,0 +1,3 @@
1
+ import type { OpenRouterGenerationProtocol } from "./contracts.js";
2
+ /** Map a caller-owned JSON Schema into the selected native protocol. */
3
+ export declare function withOpenRouterStructuredOutput(body: Readonly<Record<string, unknown>>, protocol: OpenRouterGenerationProtocol, schema: Readonly<Record<string, unknown>>, name?: string): Record<string, unknown>;
@@ -0,0 +1,30 @@
1
+ /** Map a caller-owned JSON Schema into the selected native protocol. */
2
+ export function withOpenRouterStructuredOutput(body, protocol, schema, name = "output") {
3
+ switch (protocol) {
4
+ case "openrouter-messages": {
5
+ const current = body.output_config;
6
+ return {
7
+ ...body,
8
+ output_config: {
9
+ ...(current && typeof current === "object" && !Array.isArray(current) ? current : {}),
10
+ format: { type: "json_schema", schema },
11
+ },
12
+ };
13
+ }
14
+ case "openrouter-chat": return {
15
+ ...body,
16
+ response_format: { type: "json_schema", json_schema: { name, strict: true, schema } },
17
+ };
18
+ case "openrouter-responses": {
19
+ const current = body.text;
20
+ return {
21
+ ...body,
22
+ text: {
23
+ ...(current && typeof current === "object" && !Array.isArray(current) ? current : {}),
24
+ format: { type: "json_schema", name, strict: true, schema },
25
+ },
26
+ };
27
+ }
28
+ default: throw new Error(`Not an OpenRouter generation protocol: ${protocol}`);
29
+ }
30
+ }
@@ -0,0 +1,6 @@
1
+ import type { OpenRouterEndpoint, OpenRouterFetchOptions, OpenRouterRequestOptions } from "./contracts.js";
2
+ /** OpenRouter uses Bearer authentication for all three native protocols. */
3
+ export declare function openRouterHeaders(apiKey: string | undefined, ...sources: (HeadersInit | undefined)[]): Headers;
4
+ /** Shared native/SDK transport. Response status, headers and streaming body stay untouched. */
5
+ export declare function createOpenRouterFetch(options?: OpenRouterFetchOptions): typeof globalThis.fetch;
6
+ export declare function requestOpenRouter(endpoint: OpenRouterEndpoint, body: Readonly<Record<string, unknown>>, options?: OpenRouterRequestOptions): Promise<Response>;
@@ -0,0 +1,37 @@
1
+ import { openRouterRequestUrl } from "./endpoint.js";
2
+ /** OpenRouter uses Bearer authentication for all three native protocols. */
3
+ export function openRouterHeaders(apiKey, ...sources) {
4
+ const headers = new Headers();
5
+ for (const source of sources) {
6
+ new Headers(source).forEach((value, name) => headers.set(name, value));
7
+ }
8
+ headers.delete("x-api-key");
9
+ if (apiKey !== undefined)
10
+ headers.set("authorization", `Bearer ${apiKey}`);
11
+ return headers;
12
+ }
13
+ /** Shared native/SDK transport. Response status, headers and streaming body stay untouched. */
14
+ export function createOpenRouterFetch(options = {}) {
15
+ const fetcher = options.fetch ?? globalThis.fetch;
16
+ return (input, init) => {
17
+ const request = new Request(input, init);
18
+ return fetcher(new Request(request, {
19
+ headers: openRouterHeaders(options.apiKey, options.headers, request.headers),
20
+ }));
21
+ };
22
+ }
23
+ export function requestOpenRouter(endpoint, body, options = {}) {
24
+ const url = openRouterRequestUrl(endpoint);
25
+ const headers = new Headers(options.headers);
26
+ headers.set("content-type", "application/json");
27
+ return createOpenRouterFetch({
28
+ apiKey: endpoint.apiKey,
29
+ headers: endpoint.headers,
30
+ fetch: options.fetch,
31
+ })(url, {
32
+ method: "POST",
33
+ headers,
34
+ body: JSON.stringify(body),
35
+ signal: options.signal,
36
+ });
37
+ }
package/package.json CHANGED
@@ -1,6 +1,29 @@
1
1
  {
2
2
  "name": "@springbrand/model-client",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0-alpha.1",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "files": [
7
+ "dist",
8
+ "README.md"
9
+ ],
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "engines": {
20
+ "node": ">=22.19.0"
21
+ },
22
+ "devDependencies": {
23
+ "typescript": "6.0.3"
24
+ },
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.json",
27
+ "check": "tsc -p tsconfig.json --noEmit"
28
+ }
6
29
  }