@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 +77 -2
- package/dist/contracts.d.ts +21 -0
- package/dist/contracts.js +1 -0
- package/dist/endpoint.d.ts +3 -0
- package/dist/endpoint.js +11 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/routing.d.ts +2 -0
- package/dist/routing.js +13 -0
- package/dist/structured-output.d.ts +3 -0
- package/dist/structured-output.js +30 -0
- package/dist/transport.d.ts +6 -0
- package/dist/transport.js +37 -0
- package/package.json +26 -3
package/README.md
CHANGED
|
@@ -1,3 +1,78 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @springbrand/model-client
|
|
2
2
|
|
|
3
|
-
|
|
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 {};
|
package/dist/endpoint.js
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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";
|
package/dist/routing.js
ADDED
|
@@ -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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
}
|