@orkestrate/sdk 0.1.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 ADDED
@@ -0,0 +1,108 @@
1
+ # `@orkestrate/sdk`
2
+
3
+ Publisher HTTP handler for [Orkestrate](https://orkestrate.space).
4
+
5
+ **What you get:** secret check, session id, caller model (BYOM), JSON wire.
6
+ **What you own:** chat history — same as any AI SDK app. Use `sessionId` as your chat key.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm install @orkestrate/sdk ai
12
+ ```
13
+
14
+ ## Primitives (any framework)
15
+
16
+ ```ts
17
+ import { verifyRequest, parseRequest, buildModel, respond } from "@orkestrate/sdk";
18
+
19
+ async function handler(request: Request) {
20
+ verifyRequest(request, process.env.ORKESTRATE_SECRET!);
21
+ const { action, sessionId, message, modelConfig, messages } =
22
+ await parseRequest(request);
23
+
24
+ switch (action) {
25
+ case "ping":
26
+ return respond.ok();
27
+ case "end_session":
28
+ return respond.ok();
29
+ case "start_session":
30
+ case "send_message": {
31
+ const model = buildModel(modelConfig!);
32
+ const reply = await runAgent({
33
+ message: message!,
34
+ model,
35
+ sessionId: sessionId!,
36
+ messages: messages!,
37
+ });
38
+ return respond.reply(reply);
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ ## Usage with AI SDK (convenience)
45
+
46
+ Same primitives, but `createOrkestrateHandler` wires them for you:
47
+
48
+ ```ts
49
+ // app/api/orkestrate/route.ts
50
+ import { createOrkestrateHandler } from "@orkestrate/sdk";
51
+ import { generateText } from "ai";
52
+
53
+ export const { GET, POST } = createOrkestrateHandler({
54
+ secret: process.env.ORKESTRATE_SECRET!,
55
+
56
+ async onTurn({ message, model, messages, callerId }) {
57
+ // messages is the full conversation — gateway handles storage
58
+ const result = await generateText({ model, messages });
59
+ return { reply: result.text };
60
+ },
61
+ });
62
+ ```
63
+
64
+ ## Wire (gateway → you)
65
+
66
+ | Header | When |
67
+ |--------|------|
68
+ | `Authorization: Bearer <secret>` | always on POST |
69
+ | `X-Orkestrate-Action` | `start_session` \| `send_message` \| `end_session` \| `ping` |
70
+ | `X-Orkestrate-Session-Id` | open / send / close |
71
+ | `X-Orkestrate-Model` | open / send — base64url JSON `{ provider, model, apiKey, baseURL? }` |
72
+ | `X-Orkestrate-Caller-Id` | optional |
73
+
74
+ Body open/send: `{ "message": "...", "messages": [{ role, content }, ...] }`
75
+ Success: `{ "reply": "..." }` or `{ "ok": true }`
76
+
77
+ Session ids are **minted by Orkestrate**. You never invent them for this path.
78
+
79
+ ## Error codes
80
+
81
+ | Code | HTTP | Meaning |
82
+ |------|------|---------|
83
+ | `UNAUTHORIZED` | 401 | Bad or missing `Authorization` header |
84
+ | `BAD_REQUEST` | 400 | Missing header, invalid body, bad model config |
85
+ | `MODEL_ERROR` | 400 | `buildModel` rejected the config (e.g. unknown provider) |
86
+ | `SESSION_NOT_FOUND` | 500 | Session id does not exist |
87
+ | `SESSION_EXPIRED` | 500 | Session has expired (idle or hard TTL) |
88
+ | `SESSION_STALE` | 500 | Session data is stale |
89
+ | `CONFLICT` | 500 | Concurrent operation on the same session |
90
+ | `LIMIT_EXCEEDED` | 500 | Turn limit or rate limit hit |
91
+ | `INTERNAL` | 500 | `onTurn` threw or returned an empty reply |
92
+
93
+ ## Notes
94
+
95
+ - **`ai` peer dependency:** `npm install @orkestrate/sdk ai` if you use `createOrkestrateHandler` or call `generateText`. If you only use the primitives, you can ignore the peer dep warning — `ai` is not required at runtime.
96
+ - **Streaming:** V1 is text-turn only. `respond.reply` sends one string. Streaming (`streamText`, SSE) is not yet supported.
97
+ - **`custom` provider:** Assumes an OpenAI-compatible API (OpenAI SDK shape). For Ollama, vLLM, etc. that serve an OpenAI-compatible `/chat/completions`.
98
+ - **Package size:** `@orkestrate/sdk` depends on `@ai-sdk/openai`, `@ai-sdk/anthropic`, and `@ai-sdk/google` regardless of which provider you use.
99
+
100
+ ## Not included (on purpose)
101
+
102
+ - Built-in chat store
103
+ - MCP server
104
+ - OAuth / dashboard
105
+
106
+ ## License
107
+
108
+ MIT
package/dist/auth.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Verify that `request` carries a valid `Authorization: Bearer <secret>`.
3
+ * Throws `OrkestrateError("UNAUTHORIZED")` on failure.
4
+ */
5
+ export declare function verifyRequest(request: Request, secret: string): void;
package/dist/auth.js ADDED
@@ -0,0 +1,41 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+ import { OrkestrateError } from "./errors";
3
+ /**
4
+ * Verify that `request` carries a valid `Authorization: Bearer <secret>`.
5
+ * Throws `OrkestrateError("UNAUTHORIZED")` on failure.
6
+ */
7
+ export function verifyRequest(request, secret) {
8
+ if (!secret) {
9
+ throw new OrkestrateError("INTERNAL", "Publisher secret is not configured", 500);
10
+ }
11
+ const header = request.headers.get("authorization");
12
+ if (!header) {
13
+ throw new OrkestrateError("UNAUTHORIZED", "Missing Authorization header");
14
+ }
15
+ const match = /^Bearer\s+(.+)$/i.exec(header.trim());
16
+ if (!match?.[1]) {
17
+ throw new OrkestrateError("UNAUTHORIZED", "Authorization must be Bearer <secret>");
18
+ }
19
+ const token = match[1].trim();
20
+ if (!secretsEqual(token, secret)) {
21
+ throw new OrkestrateError("UNAUTHORIZED", "Invalid secret");
22
+ }
23
+ }
24
+ /**
25
+ * Constant-time comparison of two strings.
26
+ *
27
+ * On length mismatch we compare `a` against itself so that callers cannot
28
+ * distinguish "wrong length" from "wrong content" by timing alone. The
29
+ * branch (return false vs. continue) still leaks ~1 bit — acceptable for a
30
+ * shared-secret check against remote timing noise but not for password
31
+ * hashing.
32
+ */
33
+ function secretsEqual(a, b) {
34
+ const aBuf = Buffer.from(a, "utf8");
35
+ const bBuf = Buffer.from(b, "utf8");
36
+ if (aBuf.length !== bBuf.length) {
37
+ timingSafeEqual(aBuf, aBuf);
38
+ return false;
39
+ }
40
+ return timingSafeEqual(aBuf, bBuf);
41
+ }
@@ -0,0 +1,18 @@
1
+ /** Wire error codes returned to the gateway. */
2
+ export type OrkestrateErrorCode = "UNAUTHORIZED" | "BAD_REQUEST" | "MODEL_ERROR" | "SESSION_NOT_FOUND" | "SESSION_EXPIRED" | "SESSION_STALE" | "CONFLICT" | "LIMIT_EXCEEDED" | "INTERNAL";
3
+ export declare class OrkestrateError extends Error {
4
+ readonly code: OrkestrateErrorCode;
5
+ readonly status: number;
6
+ constructor(code: OrkestrateErrorCode, message: string, status?: number);
7
+ }
8
+ /** Wire-format response builders. */
9
+ export declare const respond: {
10
+ ok: (extra?: Record<string, unknown>) => Response;
11
+ reply: (text: string) => Response;
12
+ error: (code: OrkestrateErrorCode, message: string, status?: number) => Response;
13
+ };
14
+ /**
15
+ * Convert any thrown error to a wire-format Response.
16
+ * @internal used by createOrkestrateHandler
17
+ */
18
+ export declare function toErrorResponse(error: unknown): Response;
package/dist/errors.js ADDED
@@ -0,0 +1,34 @@
1
+ function defaultStatus(code) {
2
+ return code === "UNAUTHORIZED"
3
+ ? 401
4
+ : code === "BAD_REQUEST" || code === "MODEL_ERROR"
5
+ ? 400
6
+ : 500;
7
+ }
8
+ export class OrkestrateError extends Error {
9
+ code;
10
+ status;
11
+ constructor(code, message, status) {
12
+ super(message);
13
+ this.name = "OrkestrateError";
14
+ this.code = code;
15
+ this.status = status ?? defaultStatus(code);
16
+ }
17
+ }
18
+ /** Wire-format response builders. */
19
+ export const respond = {
20
+ ok: (extra) => Response.json({ ok: true, ...extra }),
21
+ reply: (text) => Response.json({ reply: text }),
22
+ error: (code, message, status) => Response.json({ error: { code, message } }, { status: status ?? defaultStatus(code) }),
23
+ };
24
+ /**
25
+ * Convert any thrown error to a wire-format Response.
26
+ * @internal used by createOrkestrateHandler
27
+ */
28
+ export function toErrorResponse(error) {
29
+ if (error instanceof OrkestrateError) {
30
+ return respond.error(error.code, error.message, error.status);
31
+ }
32
+ const message = error instanceof Error ? error.message : "Internal error";
33
+ return respond.error("INTERNAL", message);
34
+ }
@@ -0,0 +1,10 @@
1
+ import type { CreateOrkestrateHandlerOptions, OrkestrateHandlers } from "./types";
2
+ /**
3
+ * Create GET + POST handlers for a Next.js (or any) route.
4
+ *
5
+ * Thin convenience over the SDK primitives:
6
+ * verifyRequest → parseRequest → buildModel → onTurn → respond
7
+ *
8
+ * For full control, import the primitives directly.
9
+ */
10
+ export declare function createOrkestrateHandler(options: CreateOrkestrateHandlerOptions): OrkestrateHandlers;
@@ -0,0 +1,92 @@
1
+ import { verifyRequest } from "./auth";
2
+ import { OrkestrateError, toErrorResponse } from "./errors";
3
+ import { buildModel } from "./model";
4
+ import { parseRequest } from "./protocol";
5
+ import { respond } from "./errors";
6
+ const PROTOCOL_VERSION = 1;
7
+ /**
8
+ * Create GET + POST handlers for a Next.js (or any) route.
9
+ *
10
+ * Thin convenience over the SDK primitives:
11
+ * verifyRequest → parseRequest → buildModel → onTurn → respond
12
+ *
13
+ * For full control, import the primitives directly.
14
+ */
15
+ export function createOrkestrateHandler(options) {
16
+ if (!options?.secret || typeof options.secret !== "string") {
17
+ throw new Error("createOrkestrateHandler: `secret` is required");
18
+ }
19
+ if (typeof options.onTurn !== "function") {
20
+ throw new Error("createOrkestrateHandler: `onTurn` is required");
21
+ }
22
+ const { secret, onTurn, onClose } = options;
23
+ async function GET(_request) {
24
+ return respond.ok({ protocol: PROTOCOL_VERSION });
25
+ }
26
+ async function POST(request) {
27
+ try {
28
+ verifyRequest(request, secret);
29
+ const parsed = await parseRequest(request);
30
+ switch (parsed.action) {
31
+ case "ping":
32
+ return respond.ok();
33
+ case "end_session": {
34
+ if (onClose) {
35
+ await onClose({
36
+ sessionId: parsed.sessionId,
37
+ callerId: parsed.callerId,
38
+ });
39
+ }
40
+ return respond.ok();
41
+ }
42
+ case "start_session":
43
+ case "send_message": {
44
+ if (!parsed.sessionId) {
45
+ return respond.error("BAD_REQUEST", "Missing session id");
46
+ }
47
+ if (!parsed.message) {
48
+ return respond.error("BAD_REQUEST", "Missing message");
49
+ }
50
+ if (!parsed.modelConfig) {
51
+ return respond.error("BAD_REQUEST", "Missing caller model config");
52
+ }
53
+ if (!parsed.messages || parsed.messages.length === 0) {
54
+ return respond.error("BAD_REQUEST", "Missing or empty messages");
55
+ }
56
+ const model = buildModel(parsed.modelConfig);
57
+ let result;
58
+ try {
59
+ result = await onTurn({
60
+ sessionId: parsed.sessionId,
61
+ message: parsed.message,
62
+ messages: parsed.messages,
63
+ model,
64
+ action: parsed.action,
65
+ callerId: parsed.callerId,
66
+ });
67
+ }
68
+ catch (error) {
69
+ if (error instanceof OrkestrateError)
70
+ throw error;
71
+ const msg = error instanceof Error ? error.message : "Turn failed";
72
+ throw new OrkestrateError("INTERNAL", msg);
73
+ }
74
+ if (!result ||
75
+ typeof result.reply !== "string" ||
76
+ result.reply.length === 0) {
77
+ throw new OrkestrateError("INTERNAL", "onTurn must return { reply: non-empty string }");
78
+ }
79
+ return respond.reply(result.reply);
80
+ }
81
+ default: {
82
+ const _never = parsed.action;
83
+ throw new OrkestrateError("BAD_REQUEST", `Unknown action: ${String(_never)}`);
84
+ }
85
+ }
86
+ }
87
+ catch (error) {
88
+ return toErrorResponse(error);
89
+ }
90
+ }
91
+ return { GET, POST };
92
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @orkestrate/sdk
3
+ *
4
+ * Publisher SDK for the Orkestrate agent gateway.
5
+ *
6
+ * Primitives (any framework):
7
+ * verifyRequest — authenticate incoming gateway requests
8
+ * parseRequest — decode gateway headers + body
9
+ * buildModel — build an AI SDK LanguageModel from caller's BYOM config
10
+ * respond — wire-format response builders (.ok / .reply / .error)
11
+ *
12
+ * Convenience (AI SDK):
13
+ * createOrkestrateHandler — calls the above in order for generateText users
14
+ *
15
+ * Types:
16
+ * CallerModelConfig, ParsedRequest, SessionMessage, TurnContext, CloseContext,
17
+ * TurnResult, CreateOrkestrateHandlerOptions, OrkestrateHandlers, OrkestrateAction
18
+ */
19
+ export { verifyRequest } from "./auth";
20
+ export { parseRequest, encodeModelConfig } from "./protocol";
21
+ export { buildModel } from "./model";
22
+ export { respond, OrkestrateError } from "./errors";
23
+ export type { OrkestrateErrorCode } from "./errors";
24
+ export { createOrkestrateHandler } from "./handler";
25
+ export type { CallerModelConfig, CloseContext, CreateOrkestrateHandlerOptions, OrkestrateAction, OrkestrateHandlers, ParsedRequest, SessionMessage, TurnContext, TurnResult, } from "./types";
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @orkestrate/sdk
3
+ *
4
+ * Publisher SDK for the Orkestrate agent gateway.
5
+ *
6
+ * Primitives (any framework):
7
+ * verifyRequest — authenticate incoming gateway requests
8
+ * parseRequest — decode gateway headers + body
9
+ * buildModel — build an AI SDK LanguageModel from caller's BYOM config
10
+ * respond — wire-format response builders (.ok / .reply / .error)
11
+ *
12
+ * Convenience (AI SDK):
13
+ * createOrkestrateHandler — calls the above in order for generateText users
14
+ *
15
+ * Types:
16
+ * CallerModelConfig, ParsedRequest, SessionMessage, TurnContext, CloseContext,
17
+ * TurnResult, CreateOrkestrateHandlerOptions, OrkestrateHandlers, OrkestrateAction
18
+ */
19
+ /* Primitives */
20
+ export { verifyRequest } from "./auth";
21
+ export { parseRequest, encodeModelConfig } from "./protocol";
22
+ export { buildModel } from "./model";
23
+ export { respond, OrkestrateError } from "./errors";
24
+ /* Convenience */
25
+ export { createOrkestrateHandler } from "./handler";
@@ -0,0 +1,7 @@
1
+ import type { LanguageModel } from "ai";
2
+ import type { CallerModelConfig } from "./types";
3
+ /**
4
+ * Build an AI SDK language model from the caller's BYOM config.
5
+ * Eng never pastes provider keys for Orkestrate traffic.
6
+ */
7
+ export declare function buildModel(config: CallerModelConfig): LanguageModel;
package/dist/model.js ADDED
@@ -0,0 +1,52 @@
1
+ import { createAnthropic } from "@ai-sdk/anthropic";
2
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
3
+ import { createOpenAI } from "@ai-sdk/openai";
4
+ import { OrkestrateError } from "./errors";
5
+ /**
6
+ * Build an AI SDK language model from the caller's BYOM config.
7
+ * Eng never pastes provider keys for Orkestrate traffic.
8
+ */
9
+ export function buildModel(config) {
10
+ try {
11
+ switch (config.provider) {
12
+ case "openai": {
13
+ const openai = createOpenAI({
14
+ apiKey: config.apiKey,
15
+ ...(config.baseURL ? { baseURL: config.baseURL } : {}),
16
+ });
17
+ return openai.chat(config.model);
18
+ }
19
+ case "anthropic": {
20
+ const anthropic = createAnthropic({
21
+ apiKey: config.apiKey,
22
+ ...(config.baseURL ? { baseURL: config.baseURL } : {}),
23
+ });
24
+ return anthropic(config.model);
25
+ }
26
+ case "google": {
27
+ const google = createGoogleGenerativeAI({
28
+ apiKey: config.apiKey,
29
+ ...(config.baseURL ? { baseURL: config.baseURL } : {}),
30
+ });
31
+ return google(config.model);
32
+ }
33
+ case "custom": {
34
+ const openai = createOpenAI({
35
+ apiKey: config.apiKey,
36
+ baseURL: config.baseURL,
37
+ });
38
+ return openai.chat(config.model);
39
+ }
40
+ default: {
41
+ const _exhaustive = config.provider;
42
+ throw new OrkestrateError("MODEL_ERROR", `Unsupported provider: ${String(_exhaustive)}`);
43
+ }
44
+ }
45
+ }
46
+ catch (error) {
47
+ if (error instanceof OrkestrateError)
48
+ throw error;
49
+ const message = error instanceof Error ? error.message : "Failed to build model";
50
+ throw new OrkestrateError("MODEL_ERROR", message);
51
+ }
52
+ }
@@ -0,0 +1,14 @@
1
+ import type { CallerModelConfig, ParsedRequest, SessionMessage } from "./types";
2
+ export declare const HEADER_SESSION_ID = "x-orkestrate-session-id";
3
+ export declare const HEADER_ACTION = "x-orkestrate-action";
4
+ export declare const HEADER_CALLER_ID = "x-orkestrate-caller-id";
5
+ export declare const HEADER_MODEL = "x-orkestrate-model";
6
+ /**
7
+ * Parse the gateway's HTTP request into typed fields.
8
+ * Throws `OrkestrateError("BAD_REQUEST")` on invalid input.
9
+ */
10
+ export declare function parseRequest(request: Request): Promise<ParsedRequest>;
11
+ /** Validate and parse messages from the body. */
12
+ export declare function parseMessages(raw: unknown, message?: string): SessionMessage[];
13
+ /** Encode model config for tests / gateway. Never log the result. */
14
+ export declare function encodeModelConfig(config: CallerModelConfig): string;
@@ -0,0 +1,132 @@
1
+ import { OrkestrateError } from "./errors";
2
+ export const HEADER_SESSION_ID = "x-orkestrate-session-id";
3
+ export const HEADER_ACTION = "x-orkestrate-action";
4
+ export const HEADER_CALLER_ID = "x-orkestrate-caller-id";
5
+ export const HEADER_MODEL = "x-orkestrate-model";
6
+ const ACTIONS = new Set([
7
+ "start_session",
8
+ "send_message",
9
+ "end_session",
10
+ "ping",
11
+ ]);
12
+ /**
13
+ * Parse the gateway's HTTP request into typed fields.
14
+ * Throws `OrkestrateError("BAD_REQUEST")` on invalid input.
15
+ */
16
+ export async function parseRequest(request) {
17
+ const actionRaw = request.headers.get(HEADER_ACTION)?.trim();
18
+ if (!actionRaw || !ACTIONS.has(actionRaw)) {
19
+ throw new OrkestrateError("BAD_REQUEST", `Missing or invalid ${HEADER_ACTION}. Expected start_session | send_message | end_session | ping`);
20
+ }
21
+ const action = actionRaw;
22
+ const sessionId = request.headers.get(HEADER_SESSION_ID)?.trim() || undefined;
23
+ const callerId = request.headers.get(HEADER_CALLER_ID)?.trim() || undefined;
24
+ let body = {};
25
+ const text = await request.text();
26
+ if (text.trim()) {
27
+ try {
28
+ body = JSON.parse(text);
29
+ }
30
+ catch {
31
+ throw new OrkestrateError("BAD_REQUEST", "Body must be JSON");
32
+ }
33
+ }
34
+ if (body !== null && typeof body !== "object") {
35
+ throw new OrkestrateError("BAD_REQUEST", "Body must be a JSON object");
36
+ }
37
+ const record = (body ?? {});
38
+ const message = typeof record.message === "string" ? record.message : undefined;
39
+ let modelConfig;
40
+ const modelHeader = request.headers.get(HEADER_MODEL)?.trim();
41
+ if (modelHeader) {
42
+ modelConfig = decodeModelConfig(modelHeader);
43
+ }
44
+ let messages;
45
+ if (action === "start_session" || action === "send_message") {
46
+ messages = parseMessages(record.messages, message);
47
+ }
48
+ return { action, sessionId, callerId, message, modelConfig, messages };
49
+ }
50
+ function decodeModelConfig(header) {
51
+ let json;
52
+ try {
53
+ json = Buffer.from(header, "base64url").toString("utf8");
54
+ }
55
+ catch {
56
+ throw new OrkestrateError("BAD_REQUEST", `${HEADER_MODEL} must be base64url-encoded JSON`);
57
+ }
58
+ let parsed;
59
+ try {
60
+ parsed = JSON.parse(json);
61
+ }
62
+ catch {
63
+ throw new OrkestrateError("BAD_REQUEST", `${HEADER_MODEL} must be base64url-encoded JSON`);
64
+ }
65
+ if (!parsed || typeof parsed !== "object") {
66
+ throw new OrkestrateError("BAD_REQUEST", "Invalid model config");
67
+ }
68
+ const o = parsed;
69
+ const provider = o.provider;
70
+ const model = o.model;
71
+ const apiKey = o.apiKey;
72
+ const baseURL = o.baseURL;
73
+ if (provider !== "openai" &&
74
+ provider !== "anthropic" &&
75
+ provider !== "google" &&
76
+ provider !== "custom") {
77
+ throw new OrkestrateError("BAD_REQUEST", 'model.provider must be "openai" | "anthropic" | "google" | "custom"');
78
+ }
79
+ if (typeof model !== "string" || !model) {
80
+ throw new OrkestrateError("BAD_REQUEST", "model.model must be a non-empty string");
81
+ }
82
+ if (typeof apiKey !== "string" || !apiKey) {
83
+ throw new OrkestrateError("BAD_REQUEST", "model.apiKey must be a non-empty string");
84
+ }
85
+ if (baseURL !== undefined && typeof baseURL !== "string") {
86
+ throw new OrkestrateError("BAD_REQUEST", "model.baseURL must be a string when set");
87
+ }
88
+ if (provider === "custom" && !baseURL) {
89
+ throw new OrkestrateError("BAD_REQUEST", 'model.baseURL is required when provider is "custom"');
90
+ }
91
+ return {
92
+ provider,
93
+ model,
94
+ apiKey,
95
+ ...(typeof baseURL === "string" ? { baseURL } : {}),
96
+ };
97
+ }
98
+ /** Validate and parse messages from the body. */
99
+ export function parseMessages(raw, message) {
100
+ if (!Array.isArray(raw)) {
101
+ throw new OrkestrateError("BAD_REQUEST", "Body.messages must be a non-empty array of { role, content }");
102
+ }
103
+ if (raw.length === 0) {
104
+ throw new OrkestrateError("BAD_REQUEST", "Body.messages must be a non-empty array of { role, content }");
105
+ }
106
+ for (const m of raw) {
107
+ if (!m || typeof m !== "object") {
108
+ throw new OrkestrateError("BAD_REQUEST", "Each messages entry must be { role: 'user' | 'assistant', content: string }");
109
+ }
110
+ const o = m;
111
+ if (o.role !== "user" && o.role !== "assistant") {
112
+ throw new OrkestrateError("BAD_REQUEST", "Each messages entry must have role 'user' or 'assistant'");
113
+ }
114
+ if (typeof o.content !== "string" || !o.content) {
115
+ throw new OrkestrateError("BAD_REQUEST", "Each messages entry must have non-empty content string");
116
+ }
117
+ }
118
+ const msgs = raw;
119
+ // Consistency: last message must match the `message` field
120
+ const last = msgs[msgs.length - 1];
121
+ if (last.role !== "user") {
122
+ throw new OrkestrateError("BAD_REQUEST", "Last message in messages must be from user");
123
+ }
124
+ if (message !== undefined && last.content !== message) {
125
+ throw new OrkestrateError("BAD_REQUEST", "Body.message must match the last user message in messages");
126
+ }
127
+ return msgs;
128
+ }
129
+ /** Encode model config for tests / gateway. Never log the result. */
130
+ export function encodeModelConfig(config) {
131
+ return Buffer.from(JSON.stringify(config), "utf8").toString("base64url");
132
+ }
@@ -0,0 +1,68 @@
1
+ import type { LanguageModel } from "ai";
2
+ /** Actions the gateway may send on POST. */
3
+ export type OrkestrateAction = "start_session" | "send_message" | "end_session" | "ping";
4
+ /**
5
+ * Caller model config (BYOM). Sent by the gateway as
6
+ * `X-Orkestrate-Model` (base64url JSON). Built into an AI SDK model for you.
7
+ */
8
+ export type CallerModelConfig = {
9
+ provider: "openai" | "anthropic" | "google" | "custom";
10
+ model: string;
11
+ apiKey: string;
12
+ /** Required for `custom`; optional override for openai-compatible hosts. */
13
+ baseURL?: string;
14
+ };
15
+ /** A single turn in the session conversation history. */
16
+ export type SessionMessage = {
17
+ role: "user" | "assistant";
18
+ content: string;
19
+ };
20
+ /** Result of parsing a gateway request. */
21
+ export type ParsedRequest = {
22
+ action: OrkestrateAction;
23
+ sessionId?: string;
24
+ callerId?: string;
25
+ message?: string;
26
+ modelConfig?: CallerModelConfig;
27
+ /** Full conversation history, including the latest user message. */
28
+ messages?: SessionMessage[];
29
+ };
30
+ export type TurnContext = {
31
+ /** Gateway-minted session id. Use this as your chat id. */
32
+ sessionId: string;
33
+ /** The new user text for this turn (also last element of `messages`). */
34
+ message: string;
35
+ /** Full conversation history including the latest user turn. */
36
+ messages: SessionMessage[];
37
+ /** Ready for `generateText` / `streamText` / agents. */
38
+ model: LanguageModel;
39
+ action: "start_session" | "send_message";
40
+ /** Opaque Clerk user id when the gateway sends it. */
41
+ callerId?: string;
42
+ };
43
+ export type CloseContext = {
44
+ sessionId: string;
45
+ callerId?: string;
46
+ };
47
+ export type TurnResult = {
48
+ reply: string;
49
+ };
50
+ export type CreateOrkestrateHandlerOptions = {
51
+ /** Shared secret from the Orkestrate dashboard (registration). */
52
+ secret: string;
53
+ /**
54
+ * Run one agent turn.
55
+ * The gateway forwards `messages` (full conversation history) on every turn.
56
+ * Eng can feed `ctx.messages` directly into `generateText` — no DB needed.
57
+ */
58
+ onTurn: (ctx: TurnContext) => Promise<TurnResult>;
59
+ /**
60
+ * Optional cleanup when the gateway closes a session.
61
+ * Safe to omit; response is still `{ ok: true }`.
62
+ */
63
+ onClose?: (ctx: CloseContext) => Promise<void>;
64
+ };
65
+ export type OrkestrateHandlers = {
66
+ GET: (request: Request) => Promise<Response>;
67
+ POST: (request: Request) => Promise<Response>;
68
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@orkestrate/sdk",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Publisher HTTP handler for Orkestrate — auth, session id, BYOM model, your turn.",
6
+ "license": "MIT",
7
+ "main": "./src/index.ts",
8
+ "types": "./src/index.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./src/index.ts",
12
+ "import": "./src/index.ts",
13
+ "default": "./src/index.ts"
14
+ }
15
+ },
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "main": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js",
24
+ "default": "./dist/index.js"
25
+ }
26
+ }
27
+ },
28
+ "scripts": {
29
+ "build": "tsc -p tsconfig.build.json",
30
+ "prepublishOnly": "npm run build && npm test",
31
+ "test": "vitest run",
32
+ "test:watch": "vitest",
33
+ "typecheck": "tsc --noEmit"
34
+ },
35
+ "dependencies": {
36
+ "@ai-sdk/anthropic": "^4.0.15",
37
+ "@ai-sdk/google": "^4.0.17",
38
+ "@ai-sdk/openai": "^4.0.15"
39
+ },
40
+ "peerDependencies": {
41
+ "ai": "^7.0.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^26.1.1",
45
+ "ai": "^7.0.29",
46
+ "typescript": "~6.0.3",
47
+ "vitest": "^4.1.10"
48
+ },
49
+ "files": [
50
+ "dist",
51
+ "README.md"
52
+ ],
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/system1970/orkestrate.git",
56
+ "directory": "packages/sdk"
57
+ },
58
+ "homepage": "https://orkestrate.space"
59
+ }