diff --git a/core/llm/autodetect.ts b/core/llm/autodetect.ts
index c8511554b8b..d822a46d3eb 100644
--- a/core/llm/autodetect.ts
+++ b/core/llm/autodetect.ts
@@ -69,6 +69,7 @@ const PROVIDER_HANDLES_TEMPLATING: string[] = [
"xAI",
"minimax",
"groq",
+ "haven",
"gemini",
"docker",
"nous",
diff --git a/core/llm/llms/Haven.ts b/core/llm/llms/Haven.ts
new file mode 100644
index 00000000000..eaa0cddeee9
--- /dev/null
+++ b/core/llm/llms/Haven.ts
@@ -0,0 +1,187 @@
+// Pull the ambient haven-proxy module declarations into every tsconfig that
+// compiles this file (gui and binary type-check core sources with their own
+// programs, which don't include core's .d.ts files by default).
+///
+import { LLMOptions } from "../../index.js";
+import { LlmApiRequestType } from "../openaiTypeConverters.js";
+
+import type { SecureRelay } from "haven-proxy/relay";
+
+import OpenAI from "./OpenAI.js";
+
+const HAVEN_API_ROOT = "https://ankara.aquabtc.com/api/v1/haven";
+
+// Context windows from haven-proxy's builtin catalog (defaults.js). The live
+// list is GET {root}/pricing/; these only refresh with extension releases.
+const MODEL_CONTEXT_LENGTHS: Record = {
+ "glm-5-2": 200_000,
+ "kimi-k3": 200_000,
+};
+const DEFAULT_HAVEN_CONTEXT_LENGTH = 131_072;
+
+/**
+ * Haven is JAN3's private AI chat API: request bodies are HPKE-encrypted
+ * end-to-end to a Tinfoil enclave (EHBP), so the server operator never sees
+ * prompts or completions. The encryption happens here, in-process, via the
+ * haven-proxy package's relay core — no localhost proxy needed.
+ *
+ * Auth is the plaintext outer header `X-Api-Key: hvn1_…`, injected by the
+ * relay itself and sent only to the Haven origin over HTTPS. The key is
+ * resolved from Continue config -> HAVEN_API_KEY env -> ~/.haven-proxy/config.json,
+ * so it never has to live in a shareable config file.
+ *
+ * Known tradeoff, accepted for v1: haven-proxy's relay lazily wraps
+ * `globalThis.fetch` process-wide (AsyncLocalStorage-scoped; a pure
+ * passthrough for non-relay URLs) to capture enclave error bodies and honor
+ * abort signals. Same behavior OpenCode users already run.
+ */
+class Haven extends OpenAI {
+ static providerName = "haven";
+
+ static defaultOptions: Partial = {
+ // Trailing slash matters: _getEndpoint does new URL(endpoint, apiBase).
+ apiBase: `${HAVEN_API_ROOT}/`,
+ model: "gpt-oss-120b",
+ };
+
+ // Force every request through this.fetch instead of the openai SDK adapter.
+ protected useOpenAIAdapterFor: (LlmApiRequestType | "*")[] = [];
+
+ private havenApiRoot: string;
+ private relayPromise?: Promise;
+
+ constructor(options: LLMOptions) {
+ super(options);
+ this.havenApiRoot = (this.apiBase ?? `${HAVEN_API_ROOT}/`).replace(
+ /\/+$/,
+ "",
+ );
+ if (!options.contextLength) {
+ this._contextLength =
+ MODEL_CONTEXT_LENGTHS[options.model] ?? DEFAULT_HAVEN_CONTEXT_LENGTH;
+ }
+ }
+
+ // The hvn1_ key must never ride in plaintext auth headers; the relay sends
+ // X-Api-Key itself, and only to the Haven origin.
+ protected _getHeaders() {
+ return { "Content-Type": "application/json" } as any;
+ }
+
+ private getRelay(): Promise {
+ if (!this.relayPromise) {
+ this.relayPromise = this.createRelay();
+ // A failure (e.g. missing key) must not be cached, so that fixing the
+ // env var or config works without reloading the extension.
+ this.relayPromise.catch(() => {
+ this.relayPromise = undefined;
+ });
+ }
+ return this.relayPromise;
+ }
+
+ private async createRelay(): Promise {
+ const { createSecureRelay } = await import("haven-proxy/relay");
+
+ let apiKey = this.apiKey;
+ if (!apiKey) {
+ // loadConfig applies the HAVEN_API_KEY env override before the file.
+ const { loadConfig } = await import("haven-proxy/config");
+ try {
+ apiKey = loadConfig().cfg.apiKey;
+ } catch {}
+ }
+ if (!apiKey) {
+ throw new Error(
+ "Haven API key not found. Set `apiKey` in your Continue config, " +
+ "set the HAVEN_API_KEY environment variable, or run " +
+ "`npx github:jan3dev/haven-proxy login`.",
+ );
+ }
+
+ const relay = createSecureRelay({
+ havenApiRoot: this.havenApiRoot,
+ apiKey,
+ });
+
+ // Fire-and-forget: pre-warm the enclave attestation and learn which
+ // models the backend can actually serve (precise "unknown model" errors).
+ relay.ready().catch(() => {});
+ import("haven-proxy/catalog")
+ .then(({ resolveCatalog }) => resolveCatalog(this.havenApiRoot))
+ .then(({ servableIds }) => relay.setServableModels(servableIds))
+ .catch(() => {});
+
+ return relay;
+ }
+
+ fetch(url: RequestInfo | URL, init?: RequestInit): Promise {
+ const urlStr = typeof url === "string" ? url : url.toString();
+ const method = (init?.method ?? "GET").toUpperCase();
+ if (
+ method !== "POST" ||
+ !urlStr.startsWith(this.havenApiRoot) ||
+ !urlStr.includes("/chat/completions")
+ ) {
+ // Non-chat endpoints go over plain HTTPS; headers are already key-free.
+ return super.fetch(url, init);
+ }
+ return this.havenChatFetch(init);
+ }
+
+ private async havenChatFetch(init?: RequestInit): Promise {
+ const relay = await this.getRelay();
+ const body: Record =
+ typeof init?.body === "string" ? JSON.parse(init.body) : {};
+ const result = await relay.relay(body, {
+ signal: init?.signal ?? undefined,
+ });
+
+ if (result.aborted) {
+ throw new DOMException("The request was aborted.", "AbortError");
+ }
+ if (!result.ok) {
+ const { status, message, type, code } = result.error ?? {
+ status: 502,
+ message: "Unknown Haven relay error",
+ };
+ // The relay already classified the error and retried a stale
+ // attestation key once, so throw instead of returning a non-ok
+ // Response — no second pass through Continue's backoff/parseError.
+ throw new Error(
+ `HTTP ${status} ${type ?? "haven_error"} from Haven${
+ code ? ` (${code})` : ""
+ }\n\n${message}`,
+ );
+ }
+
+ const sseHeaders = { "Content-Type": "text/event-stream" };
+ if (result.stream) {
+ return new Response(result.stream, { status: 200, headers: sseHeaders });
+ }
+
+ if (result.wantStream) {
+ // The deployment answered stream:true with buffered JSON — synthesize
+ // the SSE frames the caller is already set up to consume.
+ const { sseLinesFor } = await import("haven-proxy/relay");
+ const lines = sseLinesFor(result.completion, result.includeUsage);
+ const encoder = new TextEncoder();
+ const stream = new ReadableStream({
+ start(controller) {
+ for (const line of lines) {
+ controller.enqueue(encoder.encode(line));
+ }
+ controller.close();
+ },
+ });
+ return new Response(stream, { status: 200, headers: sseHeaders });
+ }
+
+ return new Response(JSON.stringify(result.completion), {
+ status: 200,
+ headers: { "Content-Type": "application/json" },
+ });
+ }
+}
+
+export default Haven;
diff --git a/core/llm/llms/Haven.vitest.ts b/core/llm/llms/Haven.vitest.ts
new file mode 100644
index 00000000000..53838814711
--- /dev/null
+++ b/core/llm/llms/Haven.vitest.ts
@@ -0,0 +1,279 @@
+import { beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
+
+import Haven from "./Haven.js";
+
+// test/vitest.setup.ts swaps global Response for node-fetch's, which cannot
+// carry a web ReadableStream body (it stringifies it, so streamSse sees
+// garbage). Haven builds Responses from web streams like the runtime
+// (undici) supports, so restore the native Response for this suite.
+const NativeResponse = globalThis.Response;
+beforeAll(() => {
+ globalThis.Response = NativeResponse;
+});
+
+// Mutable state the haven-proxy mocks read on each call.
+const mockState: {
+ relayResult: any;
+ relayCalls: { body: any; opts: any }[];
+ createSecureRelayCalls: any[];
+ loadConfigResult: any;
+} = {
+ relayResult: undefined,
+ relayCalls: [],
+ createSecureRelayCalls: [],
+ loadConfigResult: { cfg: { apiKey: "hvn1_from_config" }, path: "/mock" },
+};
+
+const mockRelay = {
+ relay: vi.fn(async (body: any, opts: any) => {
+ mockState.relayCalls.push({ body, opts });
+ return mockState.relayResult;
+ }),
+ setServableModels: vi.fn(),
+ ready: vi.fn(async () => {}),
+ validate: vi.fn(async () => ({ ok: true })),
+};
+
+vi.mock("haven-proxy/relay", () => ({
+ createSecureRelay: vi.fn((opts: any) => {
+ mockState.createSecureRelayCalls.push(opts);
+ return mockRelay;
+ }),
+ sseLinesFor: (completion: any, includeUsage?: boolean) => {
+ const chunk = {
+ id: completion.id,
+ object: "chat.completion.chunk",
+ choices: [
+ {
+ index: 0,
+ delta: {
+ role: "assistant",
+ content: completion.choices[0].message.content,
+ },
+ finish_reason: "stop",
+ },
+ ],
+ ...(includeUsage ? { usage: completion.usage } : {}),
+ };
+ return [`data: ${JSON.stringify(chunk)}\n\n`, "data: [DONE]\n\n"];
+ },
+ INSUFFICIENT_BALANCE_MSG: "Your Haven balance is empty.",
+}));
+
+vi.mock("haven-proxy/config", () => ({
+ loadConfig: vi.fn(() => mockState.loadConfigResult),
+}));
+
+vi.mock("haven-proxy/catalog", () => ({
+ resolveCatalog: vi.fn(async () => ({
+ models: [],
+ servableIds: ["gpt-oss-120b", "glm-5-2"],
+ source: "builtin",
+ })),
+}));
+
+function sseStreamOf(frames: string[]): ReadableStream {
+ const encoder = new TextEncoder();
+ return new ReadableStream({
+ start(controller) {
+ for (const frame of frames) {
+ controller.enqueue(encoder.encode(frame));
+ }
+ controller.close();
+ },
+ });
+}
+
+function makeHaven(options: Record = {}) {
+ return new Haven({ model: "gpt-oss-120b", ...options } as any);
+}
+
+async function collectChat(llm: Haven, signal?: AbortSignal) {
+ const chunks: any[] = [];
+ for await (const msg of llm.streamChat(
+ [{ role: "user", content: "hi" }],
+ signal ?? new AbortController().signal,
+ )) {
+ chunks.push(msg);
+ }
+ return chunks;
+}
+
+beforeEach(() => {
+ mockState.relayResult = undefined;
+ mockState.relayCalls = [];
+ mockState.createSecureRelayCalls = [];
+ mockState.loadConfigResult = {
+ cfg: { apiKey: "hvn1_from_config" },
+ path: "/mock",
+ };
+ vi.clearAllMocks();
+});
+
+describe("Haven provider", () => {
+ test("streams chat through the relay (SSE passthrough)", async () => {
+ mockState.relayResult = {
+ ok: true,
+ wantStream: true,
+ stream: sseStreamOf([
+ 'data: {"choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"}}]}\n\n',
+ 'data: {"choices":[{"index":0,"delta":{"content":" world"}}]}\n\n',
+ "data: [DONE]\n\n",
+ ]),
+ };
+ const llm = makeHaven({ apiKey: "hvn1_direct" });
+ const chunks = await collectChat(llm);
+
+ const text = chunks
+ .map((c) => (typeof c.content === "string" ? c.content : ""))
+ .join("");
+ expect(text).toBe("Hello world");
+
+ // The relay got the parsed OpenAI body and the abort signal.
+ expect(mockState.relayCalls).toHaveLength(1);
+ expect(mockState.relayCalls[0].body.model).toBe("gpt-oss-120b");
+ expect(mockState.relayCalls[0].body.stream).toBe(true);
+ expect(mockState.relayCalls[0].opts.signal).toBeInstanceOf(AbortSignal);
+ });
+
+ test("synthesizes SSE when the relay buffered a stream request", async () => {
+ mockState.relayResult = {
+ ok: true,
+ wantStream: true,
+ includeUsage: true,
+ completion: {
+ id: "cmpl-1",
+ choices: [
+ { index: 0, message: { role: "assistant", content: "Buffered" } },
+ ],
+ usage: { prompt_tokens: 1, completion_tokens: 2 },
+ },
+ };
+ const llm = makeHaven({ apiKey: "hvn1_direct" });
+ const chunks = await collectChat(llm);
+ const text = chunks
+ .map((c) => (typeof c.content === "string" ? c.content : ""))
+ .join("");
+ expect(text).toBe("Buffered");
+ });
+
+ test("returns JSON for non-stream requests", async () => {
+ mockState.relayResult = {
+ ok: true,
+ wantStream: false,
+ completion: { id: "cmpl-2", choices: [{ message: { content: "ok" } }] },
+ };
+ const llm = makeHaven({ apiKey: "hvn1_direct" });
+ const resp = await llm.fetch(
+ "https://ankara.aquabtc.com/api/v1/haven/chat/completions",
+ {
+ method: "POST",
+ body: JSON.stringify({ model: "gpt-oss-120b", stream: false }),
+ },
+ );
+ expect(resp.headers.get("Content-Type")).toBe("application/json");
+ const json: any = await resp.json();
+ expect(json.choices[0].message.content).toBe("ok");
+ });
+
+ test("throws a parseError-shaped error on relay failure", async () => {
+ mockState.relayResult = {
+ ok: false,
+ error: {
+ status: 402,
+ message: "Your Haven balance is empty.",
+ type: "insufficient_balance",
+ },
+ };
+ const llm = makeHaven({ apiKey: "hvn1_direct" });
+ await expect(collectChat(llm)).rejects.toThrow(
+ /HTTP 402 insufficient_balance from Haven[\s\S]*balance is empty/,
+ );
+ });
+
+ test("maps relay abort to an AbortError DOMException", async () => {
+ mockState.relayResult = {
+ ok: false,
+ aborted: true,
+ error: { status: 499 },
+ };
+ const llm = makeHaven({ apiKey: "hvn1_direct" });
+ await expect(collectChat(llm)).rejects.toMatchObject({
+ name: "AbortError",
+ });
+ });
+
+ test("prefers the config-file apiKey over nothing, and options.apiKey over that", async () => {
+ mockState.relayResult = {
+ ok: true,
+ wantStream: false,
+ completion: { choices: [] },
+ };
+
+ const fromOptions = makeHaven({ apiKey: "hvn1_direct" });
+ await fromOptions.fetch(
+ "https://ankara.aquabtc.com/api/v1/haven/chat/completions",
+ { method: "POST", body: "{}" },
+ );
+ expect(mockState.createSecureRelayCalls[0].apiKey).toBe("hvn1_direct");
+
+ const fromConfig = makeHaven();
+ await fromConfig.fetch(
+ "https://ankara.aquabtc.com/api/v1/haven/chat/completions",
+ { method: "POST", body: "{}" },
+ );
+ expect(mockState.createSecureRelayCalls[1].apiKey).toBe("hvn1_from_config");
+ });
+
+ test("throws an actionable error without a key, and recovers once one exists", async () => {
+ mockState.loadConfigResult = { cfg: {}, path: "/mock" };
+ mockState.relayResult = {
+ ok: true,
+ wantStream: false,
+ completion: { choices: [] },
+ };
+ const llm = makeHaven();
+ const url = "https://ankara.aquabtc.com/api/v1/haven/chat/completions";
+
+ await expect(
+ llm.fetch(url, { method: "POST", body: "{}" }),
+ ).rejects.toThrow(/HAVEN_API_KEY/);
+
+ // The failed relay promise must not be cached.
+ mockState.loadConfigResult = {
+ cfg: { apiKey: "hvn1_late" },
+ path: "/mock",
+ };
+ const resp = await llm.fetch(url, { method: "POST", body: "{}" });
+ expect(resp.status).toBe(200);
+ expect(
+ mockState.createSecureRelayCalls[
+ mockState.createSecureRelayCalls.length - 1
+ ].apiKey,
+ ).toBe("hvn1_late");
+ });
+
+ test("never puts the key in plaintext auth headers", () => {
+ const llm = makeHaven({ apiKey: "hvn1_secret" });
+ const headers = (llm as any)._getHeaders();
+ expect(headers.Authorization).toBeUndefined();
+ expect(headers["api-key"]).toBeUndefined();
+ });
+
+ test("resolves the chat endpoint under the Haven API root", () => {
+ const llm = makeHaven({ apiKey: "hvn1_direct" });
+ const endpoint = (llm as any)._getEndpoint("chat/completions").toString();
+ expect(endpoint).toBe(
+ "https://ankara.aquabtc.com/api/v1/haven/chat/completions",
+ );
+ });
+
+ test("sets context lengths from the builtin catalog, config wins", () => {
+ expect(makeHaven().contextLength).toBe(131072);
+ expect(makeHaven({ model: "glm-5-2" }).contextLength).toBe(200000);
+ expect(makeHaven({ model: "kimi-k3" }).contextLength).toBe(200000);
+ expect(
+ makeHaven({ model: "glm-5-2", contextLength: 12345 }).contextLength,
+ ).toBe(12345);
+ });
+});
diff --git a/core/llm/llms/haven-proxy.d.ts b/core/llm/llms/haven-proxy.d.ts
new file mode 100644
index 00000000000..95feb8e9072
--- /dev/null
+++ b/core/llm/llms/haven-proxy.d.ts
@@ -0,0 +1,72 @@
+// Ambient types for the haven-proxy package (plain ESM JS, ships no types).
+// Shapes mirror haven-proxy/src/relay.js, src/config.js and src/catalog.js.
+declare module "haven-proxy/relay" {
+ export interface HavenRelayError {
+ status: number;
+ message: string;
+ type?: string;
+ code?: string;
+ retryAfter?: string;
+ }
+
+ export interface HavenRelayResult {
+ ok: boolean;
+ aborted?: boolean;
+ error?: HavenRelayError;
+ stream?: ReadableStream;
+ completion?: unknown;
+ usage?: unknown;
+ wantStream?: boolean;
+ includeUsage?: boolean;
+ }
+
+ export interface SecureRelay {
+ relay(
+ body: object,
+ opts?: { signal?: AbortSignal },
+ ): Promise;
+ setServableModels(ids: string[] | null): void;
+ ready(): Promise;
+ validate(): Promise<{ ok: boolean; reason?: string; balance?: number }>;
+ }
+
+ export function createSecureRelay(opts: {
+ havenApiRoot: string;
+ apiKey: string;
+ timeoutMs?: number;
+ }): SecureRelay;
+
+ export function sseLinesFor(
+ completion: unknown,
+ includeUsage?: boolean,
+ ): string[];
+
+ export const INSUFFICIENT_BALANCE_MSG: string;
+}
+
+declare module "haven-proxy/config" {
+ export function loadConfig(): {
+ cfg: { apiKey?: string; baseURL?: string };
+ path: string;
+ };
+}
+
+declare module "haven-proxy/catalog" {
+ export interface HavenCatalogModel {
+ id: string;
+ name?: string;
+ cost?: { input: number; output: number };
+ limit?: { context?: number; output?: number };
+ capabilities?: {
+ tool_call?: boolean;
+ attachment?: boolean;
+ reasoning?: boolean;
+ };
+ }
+
+ export function resolveCatalog(havenApiRoot: string): Promise<{
+ models: HavenCatalogModel[];
+ servableIds: string[] | null;
+ source: "backend" | "cache" | "builtin";
+ }>;
+}
diff --git a/core/llm/llms/index.ts b/core/llm/llms/index.ts
index 4978f0617f2..5efb4fefe7e 100644
--- a/core/llm/llms/index.ts
+++ b/core/llm/llms/index.ts
@@ -26,6 +26,7 @@ import Flowise from "./Flowise";
import FunctionNetwork from "./FunctionNetwork";
import Gemini from "./Gemini";
import Groq from "./Groq";
+import Haven from "./Haven";
import HuggingFaceInferenceAPI from "./HuggingFaceInferenceAPI";
import HuggingFaceTEIEmbeddingsProvider from "./HuggingFaceTEI";
import HuggingFaceTGI from "./HuggingFaceTGI";
@@ -101,6 +102,7 @@ export const LLMClasses = [
DeepInfra,
Flowise,
Groq,
+ Haven,
Fireworks,
NCompass,
Cloudflare,
diff --git a/core/llm/toolSupport.ts b/core/llm/toolSupport.ts
index 3f65473233c..ea6bde7b9aa 100644
--- a/core/llm/toolSupport.ts
+++ b/core/llm/toolSupport.ts
@@ -253,6 +253,18 @@ export const PROVIDER_TOOL_SUPPORT: Record boolean> =
const lower = model.toLowerCase();
return lower.startsWith("mercury-2");
},
+ haven: (model) => {
+ // From the Haven catalog's tool_call flags (haven-proxy defaults.js /
+ // GET {root}/pricing/). gemma4-31b and deepseek-v4-flash lack tool_call.
+ const lower = model.toLowerCase();
+ return [
+ "glm-5-2",
+ "gpt-oss-120b",
+ "gpt-oss-safeguard-120b",
+ "kimi-k3",
+ "llama3-3-70b",
+ ].some((m) => lower.startsWith(m));
+ },
deepseek: (model) => {
// https://api-docs.deepseek.com/quick_start/pricing
// https://api-docs.deepseek.com/guides/function_calling
diff --git a/core/package-lock.json b/core/package-lock.json
index 7c75889e0e4..c3819cc3577 100644
--- a/core/package-lock.json
+++ b/core/package-lock.json
@@ -37,6 +37,7 @@
"follow-redirects": "^1.15.5",
"google-auth-library": "^10.4.1",
"handlebars": "^4.7.8",
+ "haven-proxy": "github:jan3dev/haven-proxy#semver:0.x",
"http-proxy-agent": "^7.0.1",
"https-proxy-agent": "^7.0.3",
"iconv-lite": "^0.6.3",
@@ -275,6 +276,53 @@
"node": ">=12.17"
}
},
+ "node_modules/@ai-sdk/openai-compatible": {
+ "version": "3.0.36",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/openai-compatible/-/openai-compatible-3.0.36.tgz",
+ "integrity": "sha512-WxbFlnc0+wspywPprsdBx6sjTQQnJjQlDS7ON/BRHikJIhOqONFsaN3dmu2Fw7xax9TNw4fJMdPbbBfOuToSZw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/provider": "4.0.7",
+ "@ai-sdk/provider-utils": "5.0.29"
+ },
+ "engines": {
+ "node": ">=22"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.76 || ^4.1.8"
+ }
+ },
+ "node_modules/@ai-sdk/provider": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.7.tgz",
+ "integrity": "sha512-6or44XprPzKbr8zkmzosowSE0pxkvJcoojBL+mCZvPUt3kvXp3XSNqeVun9golb1acEfSo6yaEBRT18h2VU+1Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "json-schema": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+ },
+ "node_modules/@ai-sdk/provider-utils": {
+ "version": "5.0.29",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-5.0.29.tgz",
+ "integrity": "sha512-7EIbwXiXKGa7EFk6tDZpuZBs6lxhEJpOuHeqrDb3Vd85uYdjwkdRuHnZDVDIIb2+QTSRmyph2NrXcbvuO/KAjQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/provider": "4.0.7",
+ "@standard-schema/spec": "^1.1.0",
+ "@workflow/serde": "4.1.0",
+ "eventsource-parser": "^3.0.8",
+ "undici": "^7.28.0"
+ },
+ "engines": {
+ "node": ">=22"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.76 || ^4.1.8"
+ }
+ },
"node_modules/@anthropic-ai/sdk": {
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.62.0.tgz",
@@ -3738,6 +3786,65 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@freedomofpress/crypto-browser": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/@freedomofpress/crypto-browser/-/crypto-browser-0.1.7.tgz",
+ "integrity": "sha512-zjWmZDKdAu8g0Zq1IjBQ+sKQ/NpfzStBDFjy/qHUSMVEL4wNlNGtA7lhtw8v8asXa0yqF2QTYQ3rq6xCTQeADw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@noble/curves": "^1.6.0"
+ },
+ "peerDependencies": {
+ "@noble/curves": "^1.6.0"
+ }
+ },
+ "node_modules/@freedomofpress/crypto-browser/node_modules/@noble/curves": {
+ "version": "1.9.7",
+ "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
+ "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "1.8.0"
+ },
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@freedomofpress/crypto-browser/node_modules/@noble/hashes": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
+ "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
+ "license": "MIT",
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@freedomofpress/sigstore-browser": {
+ "version": "0.1.14",
+ "resolved": "https://registry.npmjs.org/@freedomofpress/sigstore-browser/-/sigstore-browser-0.1.14.tgz",
+ "integrity": "sha512-1dqc7HojiBcr/sJSAXjBwNz4+WGeeAJP8ENQnDxm+idVV0/YIxpfwXRNSJdNw6EXJuHviq/5kSPEBCxamPcrpQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@freedomofpress/crypto-browser": "^0.1.7",
+ "@freedomofpress/tuf-browser": "^0.1.11",
+ "@noble/curves": "^2.0.1"
+ }
+ },
+ "node_modules/@freedomofpress/tuf-browser": {
+ "version": "0.1.11",
+ "resolved": "https://registry.npmjs.org/@freedomofpress/tuf-browser/-/tuf-browser-0.1.11.tgz",
+ "integrity": "sha512-d76ohB/AS5+zI+lnbiFMX/BIK3nT18hZ8q3Na6X4vyYaSYjXkeknzhAnbx1da+8ObWLwsaZLue46haEch28qtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@freedomofpress/crypto-browser": "^0.1.7"
+ }
+ },
"node_modules/@gar/promisify": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
@@ -4483,6 +4590,62 @@
"integrity": "sha512-/cPZD907UNz55yrc/ud4wDgQKtU1TvkD9jeqZWG6J4IMmZkp6zgjkQcKA8UvpkZlcpPHvc8J17sGzLFbP/LUYg==",
"license": "MIT"
},
+ "node_modules/@noble/ciphers": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.3.0.tgz",
+ "integrity": "sha512-Clu/xdfgVTf9o7ngLOURaxePwR0j8sjclKEtVij10/jGulwFsPWCvvRgG/XjUVf8Nei+jLG6uwyXzUTGY1DQrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@noble/curves": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.3.0.tgz",
+ "integrity": "sha512-v7cY+4oWYPQszRj6ZFGzTVL7uP2TaLo1xMhWHzYC5wj0ZhOXQ5x+sBre8rF3hi8cAoi0bh1qXoovoOkdFtvqEg==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "2.3.0"
+ },
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@noble/hashes": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz",
+ "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@noble/post-quantum": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@noble/post-quantum/-/post-quantum-0.7.0.tgz",
+ "integrity": "sha512-IH2tpuGV4vBMdpCCua2BN7EuUICtmGp6DlBMNBYAYcL6QQ7eHt85GjLyD7ZT6Qx/xgIPIMqsSLDGvYqOm8Vqag==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/ciphers": "~2.3.0",
+ "@noble/curves": "~2.3.0",
+ "@noble/hashes": "~2.3.0"
+ },
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/@nodable/entities": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.1.tgz",
@@ -4724,6 +4887,24 @@
"@octokit/openapi-types": "^24.2.0"
}
},
+ "node_modules/@panva/hpke-noble": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@panva/hpke-noble/-/hpke-noble-1.1.4.tgz",
+ "integrity": "sha512-+bOeaH/9XP8FlRqSHOy2zDEAG/SMnDfvxGlBh0bIYtEvt6vP3fkInpwV/pdtde7dF1Ujw3pUVMzYfCDPQ3nZZw==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/ciphers": "^2.3.0",
+ "@noble/curves": "^2.3.0",
+ "@noble/hashes": "^2.3.0",
+ "@noble/post-quantum": "^0.7.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ },
+ "peerDependencies": {
+ "hpke": "^1.0.0"
+ }
+ },
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -5485,7 +5666,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
- "dev": true,
"license": "MIT"
},
"node_modules/@tediousjs/connection-string": {
@@ -5494,6 +5674,20 @@
"integrity": "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ==",
"license": "MIT"
},
+ "node_modules/@tinfoilsh/verifier": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@tinfoilsh/verifier/-/verifier-1.2.1.tgz",
+ "integrity": "sha512-1DPIPKtyU6YHFvobidQI+gsPJOiidrqR/1103Fe7b8x0WhAXeJ0R82MU5tegTOteOFH2OeTUXrBiPhVsCh1hhg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@freedomofpress/crypto-browser": "^0.1.7",
+ "@freedomofpress/sigstore-browser": "^0.1.14",
+ "@freedomofpress/tuf-browser": "^0.1.11"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/@tootallnate/once": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz",
@@ -5532,8 +5726,7 @@
"license": "MIT",
"dependencies": {
"@babel/types": "^7.0.0"
- },
- "peerDependencies": {}
+ }
},
"node_modules/@types/babel__template": {
"version": "7.4.4",
@@ -5554,8 +5747,7 @@
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.2"
- },
- "peerDependencies": {}
+ }
},
"node_modules/@types/caseless": {
"version": "0.12.5",
@@ -5573,8 +5765,7 @@
"dependencies": {
"@types/deep-eql": "*",
"assertion-error": "^2.0.1"
- },
- "peerDependencies": {}
+ }
},
"node_modules/@types/command-line-args": {
"version": "5.2.0",
@@ -5602,16 +5793,14 @@
"resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.2.tgz",
"integrity": "sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q==",
"dev": true,
- "license": "MIT",
- "peerDependencies": {}
+ "license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"dev": true,
- "license": "MIT",
- "peerDependencies": {}
+ "license": "MIT"
},
"node_modules/@types/follow-redirects": {
"version": "1.14.4",
@@ -5679,8 +5868,7 @@
"dependencies": {
"expect": "^29.0.0",
"pretty-format": "^29.0.0"
- },
- "peerDependencies": {}
+ }
},
"node_modules/@types/jquery": {
"version": "3.5.34",
@@ -5690,8 +5878,7 @@
"license": "MIT",
"dependencies": {
"@types/sizzle": "*"
- },
- "peerDependencies": {}
+ }
},
"node_modules/@types/jsdom": {
"version": "21.1.7",
@@ -5747,8 +5934,7 @@
"resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.6.tgz",
"integrity": "sha512-t+8/QWTAhOFlrF1IVZqKnMRJi84EgkIK5Kh0p2JV4OLywUvCwJPFxbJAl7XAow7DVIHsF+xW9f1MVzg0L6Szjw==",
"dev": true,
- "license": "MIT",
- "peerDependencies": {}
+ "license": "MIT"
},
"node_modules/@types/node": {
"version": "25.9.2",
@@ -5757,8 +5943,7 @@
"license": "MIT",
"dependencies": {
"undici-types": ">=7.24.0 <7.24.7"
- },
- "peerDependencies": {}
+ }
},
"node_modules/@types/node-fetch": {
"version": "2.6.13",
@@ -5769,8 +5954,7 @@
"dependencies": {
"@types/node": "*",
"form-data": "^4.0.4"
- },
- "peerDependencies": {}
+ }
},
"node_modules/@types/node-forge": {
"version": "1.3.14",
@@ -5780,8 +5964,7 @@
"license": "MIT",
"dependencies": {
"@types/node": "*"
- },
- "peerDependencies": {}
+ }
},
"node_modules/@types/pad-left": {
"version": "2.1.1",
@@ -5800,8 +5983,7 @@
"@types/node": "*",
"pg-protocol": "*",
"pg-types": "^2.2.0"
- },
- "peerDependencies": {}
+ }
},
"node_modules/@types/plist": {
"version": "3.0.5",
@@ -5821,8 +6003,7 @@
"license": "MIT",
"dependencies": {
"@types/node": "*"
- },
- "peerDependencies": {}
+ }
},
"node_modules/@types/request": {
"version": "2.48.13",
@@ -5835,8 +6016,7 @@
"@types/node": "*",
"@types/tough-cookie": "*",
"form-data": "^2.5.5"
- },
- "peerDependencies": {}
+ }
},
"node_modules/@types/request/node_modules/form-data": {
"version": "2.5.5",
@@ -5891,8 +6071,7 @@
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.10.tgz",
"integrity": "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==",
"dev": true,
- "license": "MIT",
- "peerDependencies": {}
+ "license": "MIT"
},
"node_modules/@types/stack-utils": {
"version": "2.0.3",
@@ -5950,6 +6129,15 @@
"@types/node-forge": "*"
}
},
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/yargs": {
"version": "17.0.35",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
@@ -5958,8 +6146,7 @@
"license": "MIT",
"dependencies": {
"@types/yargs-parser": "*"
- },
- "peerDependencies": {}
+ }
},
"node_modules/@types/yargs-parser": {
"version": "21.0.3",
@@ -6359,6 +6546,12 @@
"url": "https://opencollective.com/vitest"
}
},
+ "node_modules/@workflow/serde": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@workflow/serde/-/serde-4.1.0.tgz",
+ "integrity": "sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ==",
+ "license": "Apache-2.0"
+ },
"node_modules/@xenova/transformers": {
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/@xenova/transformers/-/transformers-2.14.0.tgz",
@@ -7459,8 +7652,7 @@
"license": "MIT",
"engines": {
"node": "*"
- },
- "optionalDependencies": {}
+ }
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
@@ -8756,6 +8948,19 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
+ "node_modules/ehbp": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/ehbp/-/ehbp-0.3.2.tgz",
+ "integrity": "sha512-t6aIXrztsQC7jGypZ4Mvc7oMP2LN1q5zAWRKZUcVbgEZgXq/OoD9MAQGFOaCv5LMexx6BOKYpEfIhM/kaRMrtg==",
+ "license": "MIT",
+ "dependencies": {
+ "@panva/hpke-noble": "^1.0.3",
+ "hpke": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/eight-colors": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/eight-colors/-/eight-colors-1.3.3.tgz",
@@ -10844,6 +11049,30 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/haven-proxy": {
+ "version": "0.5.0",
+ "resolved": "git+ssh://git@github.com/jan3dev/haven-proxy.git#7e5ee1d228db9ebf48bdce5b8d5e8db85f2a0e66",
+ "dependencies": {
+ "@ai-sdk/openai-compatible": "^3.0.11",
+ "tinfoil": "^1.1.10",
+ "zod": "^4.1.8"
+ },
+ "bin": {
+ "haven-proxy": "src/index.js"
+ },
+ "engines": {
+ "node": ">=24"
+ }
+ },
+ "node_modules/haven-proxy/node_modules/zod": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
@@ -10862,6 +11091,15 @@
"node": ">=16.9.0"
}
},
+ "node_modules/hpke": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/hpke/-/hpke-1.1.4.tgz",
+ "integrity": "sha512-cPzmFEsiyNnD7281X5WeZ461mbH+3P+rjWMSNrLO5rks7dAJvFXAyMwCmorB61pxt+jBnd0GQ6CY43TqOxmhCQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/html-encoding-sniffer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
@@ -12852,6 +13090,12 @@
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"license": "MIT"
},
+ "node_modules/json-schema": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
+ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
+ "license": "(AFL-2.1 OR BSD-3-Clause)"
+ },
"node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@@ -16844,8 +17088,7 @@
},
"engines": {
"node": "*"
- },
- "optionalDependencies": {}
+ }
},
"node_modules/split2": {
"version": "4.2.0",
@@ -17492,6 +17735,62 @@
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
"license": "MIT"
},
+ "node_modules/tinfoil": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/tinfoil/-/tinfoil-1.2.1.tgz",
+ "integrity": "sha512-goql//0KY6nViV96wBdmIT6ZZ7rYy9yEhJrtIPdg9YHw3vyDsyNsuXNq/sZgu0Bu+sm5crxQObxrMGwgN572OQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/openai-compatible": "^3.0.7",
+ "@freedomofpress/sigstore-browser": "^0.1.14",
+ "@tinfoilsh/verifier": "1.2.1",
+ "@types/ws": "^8.18.1",
+ "ehbp": "^0.3.2",
+ "openai": "^6.46.0",
+ "ws": "^8.21.0"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "ai": "^6.0.168 || ^7.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ai": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinfoil/node_modules/openai": {
+ "version": "6.49.0",
+ "resolved": "https://registry.npmjs.org/openai/-/openai-6.49.0.tgz",
+ "integrity": "sha512-aYCc0C6L864eR6WSYIwQGyXriw/nIyZx0ObvhzOEVuk0zoBDpynjSbrionWI7q65B5H8jJX0DXR9snEzM6bfPg==",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "@aws-sdk/credential-provider-node": ">=3.972.0 <4",
+ "@smithy/hash-node": ">=4.3.0 <5",
+ "@smithy/signature-v4": ">=5.4.0 <6",
+ "ws": "^8.18.0",
+ "zod": "^3.25 || ^4.0"
+ },
+ "peerDependenciesMeta": {
+ "@aws-sdk/credential-provider-node": {
+ "optional": true
+ },
+ "@smithy/hash-node": {
+ "optional": true
+ },
+ "@smithy/signature-v4": {
+ "optional": true
+ },
+ "ws": {
+ "optional": true
+ },
+ "zod": {
+ "optional": true
+ }
+ }
+ },
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -17813,8 +18112,7 @@
},
"engines": {
"node": "*"
- },
- "optionalDependencies": {}
+ }
},
"node_modules/type-check": {
"version": "0.4.0",
@@ -18056,9 +18354,9 @@
}
},
"node_modules/undici": {
- "version": "7.27.2",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.2.tgz",
- "integrity": "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==",
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
+ "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
@@ -19366,8 +19664,7 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
"integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==",
"license": "MIT",
- "optional": true,
- "optionalDependencies": {}
+ "optional": true
},
"node_modules/wink-distance": {
"version": "2.0.2",
diff --git a/core/package.json b/core/package.json
index b8441cc339c..644f033669f 100644
--- a/core/package.json
+++ b/core/package.json
@@ -82,6 +82,7 @@
"follow-redirects": "^1.15.5",
"google-auth-library": "^10.4.1",
"handlebars": "^4.7.8",
+ "haven-proxy": "github:jan3dev/haven-proxy#semver:0.x",
"http-proxy-agent": "^7.0.1",
"https-proxy-agent": "^7.0.3",
"iconv-lite": "^0.6.3",
diff --git a/extensions/vscode/config_schema.json b/extensions/vscode/config_schema.json
index fb3f4c61362..8234e690ec2 100644
--- a/extensions/vscode/config_schema.json
+++ b/extensions/vscode/config_schema.json
@@ -236,7 +236,8 @@
"ovhcloud",
"venice",
"inception",
- "tars"
+ "tars",
+ "haven"
],
"markdownEnumDescriptions": [
"### OpenAI\nUse gpt-4, gpt-3.5-turbo, or any other OpenAI model. See [here](https://openai.com/product#made-for-developers) to obtain an API key.\n\n> [Reference](https://docs.continue.dev/reference/Model%20Providers/openai)",
@@ -289,7 +290,8 @@
"### OVHcloud AI Endpoints is a serverless inference API that provides access to a curated selection of models (e.g., Llama, Mistral, Qwen, Deepseek). It is designed with security and data privacy in mind and is compliant with GDPR. To get started, create an API key on the OVHcloud [AI Endpoints website](https://endpoints.ai.cloud.ovh.net/). For more information, including pricing, visit the OVHcloud [AI Endpoints product page](https://www.ovhcloud.com/en/public-cloud/ai-endpoints/).",
"### Venice\n Venice.AI is a privacy-focused generative AI platform, allowing users to interact with open-source LLMs without storing any private user data.\nHosted models support the OpenAI API standard, providing seamless integration for users seeking privacy and flexibility.\nTo get started with the Venice API, either purchase a pro account, stake $VVV for daily inference allotments, or fund your account with USD.\nVisit the [API settings page](https://venice.ai/settings/api) or learn more at the [Venice API documentation](https://venice.ai/api).",
"### Inception\n Inception Labs offer a new generation of diffusion-based LLMs.\nVisit the [API settings page](https://platform.inceptionlabs.ai/) or learn more at the [Inception docs](https://platform.inceptionlabs.ai/docs).",
- "### TARS\nTARS is an OpenAI-compatible proxy router. To get started, obtain an API key and configure the provider in your config.json."
+ "### TARS\nTARS is an OpenAI-compatible proxy router. To get started, obtain an API key and configure the provider in your config.json.",
+ "### Haven\nHaven is JAN3's private AI chat: requests are end-to-end encrypted (HPKE) to a Tinfoil enclave, so prompts are never visible to the server operator. The `apiKey` field is optional — Haven also reads the HAVEN_API_KEY environment variable and `~/.haven-proxy/config.json` (written by `npx github:jan3dev/haven-proxy login`)."
],
"type": "string"
},
@@ -1301,6 +1303,31 @@
}
}
},
+ {
+ "if": {
+ "properties": {
+ "provider": {
+ "enum": ["haven"]
+ }
+ },
+ "required": ["provider"]
+ },
+ "then": {
+ "properties": {
+ "model": {
+ "enum": [
+ "gpt-oss-120b",
+ "gpt-oss-safeguard-120b",
+ "glm-5-2",
+ "kimi-k3",
+ "gemma4-31b",
+ "llama3-3-70b",
+ "deepseek-v4-flash"
+ ]
+ }
+ }
+ }
+ },
{
"if": {
"properties": {
diff --git a/extensions/vscode/package-lock.json b/extensions/vscode/package-lock.json
index 6fb35c7dde9..8ca56c95391 100644
--- a/extensions/vscode/package-lock.json
+++ b/extensions/vscode/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "continue",
- "version": "1.3.39",
+ "version": "1.3.41",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "continue",
- "version": "1.3.39",
+ "version": "1.3.41",
"license": "Apache-2.0",
"dependencies": {
"@continuedev/config-types": "file:../../packages/config-types",
@@ -125,6 +125,7 @@
"follow-redirects": "^1.15.5",
"google-auth-library": "^10.4.1",
"handlebars": "^4.7.8",
+ "haven-proxy": "github:jan3dev/haven-proxy#semver:0.x",
"http-proxy-agent": "^7.0.1",
"https-proxy-agent": "^7.0.3",
"iconv-lite": "^0.6.3",
@@ -7577,18 +7578,6 @@
}
}
},
- "node_modules/inquirer/node_modules/@types/node": {
- "version": "25.9.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.2.tgz",
- "integrity": "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "undici-types": ">=7.24.0 <7.24.7"
- }
- },
"node_modules/inquirer/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -7612,15 +7601,6 @@
"node": ">=8"
}
},
- "node_modules/inquirer/node_modules/undici-types": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
- "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true
- },
"node_modules/ip-address": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
@@ -13770,17 +13750,6 @@
"url": "https://opencollective.com/vitest"
}
},
- "node_modules/vite-node/node_modules/@types/node": {
- "version": "25.9.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.2.tgz",
- "integrity": "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "undici-types": ">=7.24.0 <7.24.7"
- }
- },
"node_modules/vite-node/node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@@ -13812,14 +13781,6 @@
}
}
},
- "node_modules/vite-node/node_modules/undici-types": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
- "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
- "license": "MIT",
- "optional": true,
- "peer": true
- },
"node_modules/vite-node/node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json
index 0030d3ea59c..d410929a3a9 100644
--- a/extensions/vscode/package.json
+++ b/extensions/vscode/package.json
@@ -2,7 +2,7 @@
"name": "continue",
"icon": "media/icon.png",
"author": "Continue Dev, Inc",
- "version": "1.3.40",
+ "version": "1.3.41",
"repository": {
"type": "git",
"url": "https://github.com/continuedev/continue"
diff --git a/gui/package-lock.json b/gui/package-lock.json
index 06763b6bd75..e43d7484674 100644
--- a/gui/package-lock.json
+++ b/gui/package-lock.json
@@ -142,6 +142,7 @@
"follow-redirects": "^1.15.5",
"google-auth-library": "^10.4.1",
"handlebars": "^4.7.8",
+ "haven-proxy": "github:jan3dev/haven-proxy#semver:0.x",
"http-proxy-agent": "^7.0.1",
"https-proxy-agent": "^7.0.3",
"iconv-lite": "^0.6.3",
diff --git a/packages/openai-adapters/src/apis/OpenAI.test.ts b/packages/openai-adapters/src/apis/OpenAI.test.ts
new file mode 100644
index 00000000000..f4c1bfc9e24
--- /dev/null
+++ b/packages/openai-adapters/src/apis/OpenAI.test.ts
@@ -0,0 +1,109 @@
+import { ChatCompletionChunk } from "openai/resources/index";
+import { describe, expect, test, vi } from "vitest";
+import { OpenAIApi } from "./OpenAI.js";
+
+// Build a chunk the way a given backend would emit it.
+function chunk(
+ delta: Record,
+ opts: { usage?: boolean; finish?: string } = {},
+): ChatCompletionChunk {
+ return {
+ id: "chatcmpl-test",
+ object: "chat.completion.chunk",
+ created: 0,
+ model: "test-model",
+ choices: [
+ {
+ index: 0,
+ delta,
+ finish_reason: (opts.finish ?? null) as any,
+ logprobs: null,
+ },
+ ],
+ ...(opts.usage
+ ? { usage: { prompt_tokens: 10, completion_tokens: 2, total_tokens: 12 } }
+ : {}),
+ } as ChatCompletionChunk;
+}
+
+const usageOnlyChunk = {
+ id: "chatcmpl-test",
+ object: "chat.completion.chunk",
+ created: 0,
+ model: "test-model",
+ choices: [],
+ usage: { prompt_tokens: 10, completion_tokens: 2, total_tokens: 12 },
+} as unknown as ChatCompletionChunk;
+
+function apiYielding(chunks: ChatCompletionChunk[]) {
+ const api = new OpenAIApi({
+ provider: "openai",
+ apiKey: "test",
+ apiBase: "http://127.0.0.1:0/v1",
+ } as any);
+ vi.spyOn((api as any).openai.chat.completions, "create").mockResolvedValue(
+ (async function* () {
+ for (const c of chunks) yield c;
+ })() as any,
+ );
+ return api;
+}
+
+async function collect(api: OpenAIApi) {
+ const out: ChatCompletionChunk[] = [];
+ for await (const c of api.chatCompletionStream(
+ { model: "test-model", messages: [], stream: true },
+ new AbortController().signal,
+ )) {
+ out.push(c);
+ }
+ return out;
+}
+
+function textOf(chunks: ChatCompletionChunk[]) {
+ return chunks.map((c) => c.choices?.[0]?.delta?.content ?? "").join("");
+}
+
+describe("chatCompletionStream usage handling", () => {
+ // vLLM (and the Haven relay in front of it) attaches usage to every chunk.
+ // Deferring on `usage` alone swallowed the entire completion.
+ test("keeps content when every chunk carries usage", async () => {
+ const api = apiYielding([
+ chunk({ role: "assistant", content: "" }, { usage: true }),
+ chunk({ content: "Hi" }, { usage: true }),
+ chunk({ content: " there" }, { usage: true }),
+ chunk({}, { usage: true, finish: "stop" }),
+ ]);
+
+ const out = await collect(api);
+
+ expect(textOf(out)).toBe("Hi there");
+ expect(out.at(-1)?.choices?.[0]?.finish_reason).toBe("stop");
+ });
+
+ test("still defers an OpenAI-style usage-only chunk to the end", async () => {
+ const api = apiYielding([
+ chunk({ role: "assistant", content: "" }),
+ chunk({ content: "Hi" }),
+ usageOnlyChunk,
+ chunk({}, { finish: "stop" }),
+ ]);
+
+ const out = await collect(api);
+
+ expect(textOf(out)).toBe("Hi");
+ expect(out.at(-1)?.usage?.completion_tokens).toBe(2);
+ // The usage-only chunk must not interrupt the content chunks.
+ expect(out.at(-1)?.choices?.length).toBe(0);
+ });
+
+ test("passes through a stream with no usage at all", async () => {
+ const api = apiYielding([
+ chunk({ content: "a" }),
+ chunk({ content: "b" }),
+ chunk({}, { finish: "stop" }),
+ ]);
+
+ expect(textOf(await collect(api))).toBe("ab");
+ });
+});
diff --git a/packages/openai-adapters/src/apis/OpenAI.ts b/packages/openai-adapters/src/apis/OpenAI.ts
index d0f8d30ca3a..1b44a77f8e1 100644
--- a/packages/openai-adapters/src/apis/OpenAI.ts
+++ b/packages/openai-adapters/src/apis/OpenAI.ts
@@ -161,12 +161,14 @@ export class OpenAIApi implements BaseLlmApi {
);
let lastChunkWithUsage: ChatCompletionChunk | undefined;
for await (const result of response) {
- // Check if this chunk contains usage information
- if (result.usage) {
- // Store it to emit after all content chunks
- lastChunkWithUsage = result;
- } else {
+ // Defer only a usage-*only* chunk (no choices), which is how OpenAI
+ // reports usage with stream_options.include_usage. Keying on `usage`
+ // alone would swallow the whole completion on backends like vLLM, which
+ // attach usage to every chunk.
+ if (result.choices?.length) {
yield result;
+ } else if (result.usage) {
+ lastChunkWithUsage = result;
}
}
// Emit the usage chunk at the end if we have one