@jacobbd/relay-ai 0.9.8 → 0.10.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.
@@ -16,6 +16,13 @@ type RelayCoreErrorCode = 'INVALID_ROUTE_ID' | 'ROUTE_NOT_FOUND' | 'PROVIDER_DIS
16
16
  */
17
17
  type RelayReasoningLevel = 'off' | 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
18
18
  interface CreateRelayModelOptions {
19
+ /**
20
+ * Optional stable conversation identifier for OpenCode Go. Relay sends this
21
+ * as `x-opencode-session` on every upstream model call for the returned model.
22
+ * The value is treated as opaque and bounded to OpenCode's 256-character
23
+ * header limit. Per-call `headers` supplied to the AI SDK take precedence.
24
+ */
25
+ sessionId?: string;
19
26
  /**
20
27
  * Optional sanitized transport diagnostics. Messages contain event types,
21
28
  * field names, counts, and lengths — never credentials, prompts, or bodies.
@@ -50,7 +50,7 @@ import { join as join2 } from "path";
50
50
  // package.json
51
51
  var package_default = {
52
52
  name: "@jacobbd/relay-ai",
53
- version: "0.9.8",
53
+ version: "0.10.0",
54
54
  publishConfig: {
55
55
  access: "public"
56
56
  },
@@ -2204,6 +2204,55 @@ function isRelayCoreError(err) {
2204
2204
  return err instanceof RelayCoreError;
2205
2205
  }
2206
2206
 
2207
+ // src/opencode-session.ts
2208
+ import { randomUUID as randomUUID2 } from "crypto";
2209
+ var OPENCODE_SESSION_HEADER = "x-opencode-session";
2210
+ var MAX_OPENCODE_SESSION_LENGTH = 256;
2211
+ var RELAY_USER_AGENT = `relay-ai/${VERSION}`;
2212
+ function sanitizeSessionId(value) {
2213
+ if (typeof value !== "string") return void 0;
2214
+ const normalized = value.trim();
2215
+ if (!normalized || normalized.length > MAX_OPENCODE_SESSION_LENGTH) return void 0;
2216
+ if (/[\u0000-\u001f\u007f\r\n]/.test(normalized)) return void 0;
2217
+ return normalized;
2218
+ }
2219
+ function isOpenCodeGoEndpoint(providerId, endpoint) {
2220
+ const id = providerId?.trim().toLowerCase();
2221
+ if (id === "go" || id === "opencode-go") return true;
2222
+ if (!endpoint) return false;
2223
+ try {
2224
+ const url = new URL(endpoint);
2225
+ if (url.hostname.toLowerCase() !== "opencode.ai") return false;
2226
+ return url.pathname.split("/").some((segment) => segment.toLowerCase() === "go");
2227
+ } catch {
2228
+ return false;
2229
+ }
2230
+ }
2231
+ function mergeHeaders(base, overrides) {
2232
+ const result = {};
2233
+ for (const [key, value] of Object.entries(base ?? {})) {
2234
+ if (typeof value === "string") result[key] = value;
2235
+ }
2236
+ for (const [key, value] of Object.entries(overrides ?? {})) {
2237
+ for (const existing of Object.keys(result)) {
2238
+ if (existing.toLowerCase() === key.toLowerCase()) delete result[existing];
2239
+ }
2240
+ result[key] = value;
2241
+ }
2242
+ return result;
2243
+ }
2244
+ function openCodeGoHeaders(providerId, endpoint, sessionId, baseHeaders, options) {
2245
+ if (!isOpenCodeGoEndpoint(providerId, endpoint)) return void 0;
2246
+ let normalizedSessionId = sanitizeSessionId(sessionId);
2247
+ if (!normalizedSessionId && (options?.generateFallbackSession ?? true)) {
2248
+ normalizedSessionId = `relay-${randomUUID2()}`;
2249
+ }
2250
+ return mergeHeaders(baseHeaders, {
2251
+ "User-Agent": RELAY_USER_AGENT,
2252
+ ...normalizedSessionId ? { [OPENCODE_SESSION_HEADER]: normalizedSessionId } : {}
2253
+ });
2254
+ }
2255
+
2207
2256
  // src/core/reasoning.ts
2208
2257
  var RELAY_REASONING_LEVELS = [
2209
2258
  "off",
@@ -2277,6 +2326,22 @@ async function withReasoningProviderOptions(model, providerOptions) {
2277
2326
  }
2278
2327
  });
2279
2328
  }
2329
+ async function withRequestHeaders(model, headers) {
2330
+ const { wrapLanguageModel: wrapLanguageModel2 } = await import("ai");
2331
+ return wrapLanguageModel2({
2332
+ model,
2333
+ middleware: {
2334
+ specificationVersion: "v3",
2335
+ transformParams: async ({ params }) => ({
2336
+ ...params,
2337
+ headers: mergeHeaders(
2338
+ headers,
2339
+ params.headers
2340
+ )
2341
+ })
2342
+ }
2343
+ });
2344
+ }
2280
2345
 
2281
2346
  // src/core/route-id.ts
2282
2347
  var SEPARATOR = "::";
@@ -3509,7 +3574,7 @@ function providerRefreshToken(providerId, authType, authRef) {
3509
3574
  }
3510
3575
 
3511
3576
  // src/core/antigravity-model.ts
3512
- import { randomUUID as randomUUID2 } from "crypto";
3577
+ import { randomUUID as randomUUID3 } from "crypto";
3513
3578
  var CLOUD_CODE_BASES = ANTIGRAVITY_BASE_URLS.map((base) => base.replace(/\/+$/, ""));
3514
3579
  var CLOUD_CODE_BASE = CLOUD_CODE_BASES[0];
3515
3580
  var STREAM_URLS = CLOUD_CODE_BASES.map((base) => `${base}/${ANTIGRAVITY_API_VERSION}:streamGenerateContent?alt=sse`);
@@ -3596,7 +3661,7 @@ function createCloudCodeFetch(options, fetchImpl) {
3596
3661
  const geminiBody = await readJsonBody(input, init);
3597
3662
  const envelope = {
3598
3663
  project: options.projectId,
3599
- requestId: randomUUID2(),
3664
+ requestId: randomUUID3(),
3600
3665
  model: options.modelId,
3601
3666
  userAgent: ANTIGRAVITY_USER_AGENT,
3602
3667
  requestType: "agent",
@@ -3791,7 +3856,21 @@ async function createRelayModel(routeId, options) {
3791
3856
  const registry = loadCoreRegistry();
3792
3857
  const { provider, model } = findRoute(registry, providerId, modelId, routeId);
3793
3858
  const reasoningOptions = options?.reasoning === void 0 ? void 0 : resolveReasoningProviderOptions(options.reasoning, provider, model, routeId);
3794
- const finish = (built) => reasoningOptions ? withReasoningProviderOptions(built, reasoningOptions) : Promise.resolve(built);
3859
+ const transportHeaders = openCodeGoHeaders(
3860
+ provider.id,
3861
+ model.apiUrl ?? provider.api.url,
3862
+ options?.sessionId,
3863
+ provider.api.headers,
3864
+ // The returned model may be reused across conversations, so never fabricate a
3865
+ // session here — a baked-in id would blend histories. The host passes sessionId.
3866
+ { generateFallbackSession: false }
3867
+ );
3868
+ const finish = async (built) => {
3869
+ let finished = built;
3870
+ if (reasoningOptions) finished = await withReasoningProviderOptions(finished, reasoningOptions);
3871
+ if (transportHeaders) finished = await withRequestHeaders(finished, transportHeaders);
3872
+ return finished;
3873
+ };
3795
3874
  if (isAntigravityCloudCodeRoute(provider, model)) {
3796
3875
  const apiKey2 = await resolveCredential(provider, routeId);
3797
3876
  const providerData2 = await resolveProviderOAuthProviderData(provider.authRef);