diff --git a/.changeset/login-source-backed-gateway-model.md b/.changeset/login-source-backed-gateway-model.md index 247ff0ce5..a83e34f1d 100644 --- a/.changeset/login-source-backed-gateway-model.md +++ b/.changeset/login-source-backed-gateway-model.md @@ -2,4 +2,4 @@ "eve": patch --- -`/login` now connects a Vercel account or AI Gateway key to an agent whose `model` is a gateway SDK call (such as `gateway(...)`) without trying to rewrite `agent.ts`. Previously the failed source edit discarded the connection and returned to the connection picker, leaving the agent unusable. +`/login` now works for agents whose `model` is a gateway SDK call such as `gateway("anthropic/claude-sonnet-5")`. In `eve dev`, a gateway-routed model instance is served through the connection saved by `/login` (Vercel account, AI Gateway key, or project), the same way a string model id is, and `/login` no longer tries to rewrite `agent.ts` for it. Previously the failed source edit sent you back to the connection picker after every sign-in. diff --git a/packages/eve/src/runtime/agent/resolve-model.test.ts b/packages/eve/src/runtime/agent/resolve-model.test.ts index 9e14674d0..65e7c07f0 100644 --- a/packages/eve/src/runtime/agent/resolve-model.test.ts +++ b/packages/eve/src/runtime/agent/resolve-model.test.ts @@ -1,5 +1,5 @@ import type { LanguageModel } from "ai"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ROOT_COMPILED_AGENT_NODE_ID } from "#compiler/manifest.js"; import type { CompiledModuleMap } from "#compiler/module-map.js"; @@ -72,6 +72,58 @@ describe("dynamic runtime model resolution", () => { expect(model.modelId).toBe("eve-mock/dynamic-subagent"); }); + describe("source-backed models", () => { + const source = { + logicalPath: "agent.ts", + sourceId: "agent-config", + sourceKind: "module" as const, + }; + const resolve = (model: LanguageModel) => + resolveRuntimeModelReference( + { id: typeof model === "string" ? model : `${model.provider}/${model.modelId}`, source }, + { moduleMap: createModuleMap({ default: { model } }), nodeId: undefined }, + ); + + beforeEach(() => vi.stubEnv("NODE_ENV", "production")); + + it("serves a gateway SDK instance through the /login connection in eve dev", async () => { + vi.stubEnv("EVE_DEV", "1"); + vi.stubEnv("EVE_MODEL_CONNECTION", "vercel"); + const authored = createLanguageModel("gateway", "anthropic/claude-sonnet-5"); + + const model = await resolve(authored); + + expect(model).not.toBe(authored); + if (typeof model === "string") throw new Error("expected a model instance"); + expect(model.provider).toBe("gateway"); + expect(model.modelId).toBe("anthropic/claude-sonnet-5"); + }); + + it("returns the authored gateway instance when no connection is active", async () => { + vi.stubEnv("EVE_DEV", "1"); + vi.stubEnv("EVE_MODEL_CONNECTION", ""); + const authored = createLanguageModel("gateway", "anthropic/claude-sonnet-5"); + + expect(await resolve(authored)).toBe(authored); + }); + + it("returns the authored gateway instance outside eve dev", async () => { + vi.stubEnv("EVE_DEV", ""); + vi.stubEnv("EVE_MODEL_CONNECTION", "vercel"); + const authored = createLanguageModel("gateway", "anthropic/claude-sonnet-5"); + + expect(await resolve(authored)).toBe(authored); + }); + + it("leaves a direct provider instance untouched", async () => { + vi.stubEnv("EVE_DEV", "1"); + vi.stubEnv("EVE_MODEL_CONNECTION", "vercel"); + const authored = createLanguageModel("openai.responses", "gpt-5.5"); + + expect(await resolve(authored)).toBe(authored); + }); + }); + it("loads resolver-only definitions and normalizes explicit metadata", async () => { const moduleMap = createModuleMap({ default: { diff --git a/packages/eve/src/runtime/agent/resolve-model.ts b/packages/eve/src/runtime/agent/resolve-model.ts index 356eb0a92..21b5a6a70 100644 --- a/packages/eve/src/runtime/agent/resolve-model.ts +++ b/packages/eve/src/runtime/agent/resolve-model.ts @@ -110,7 +110,23 @@ async function loadSourceBackedRuntimeModelReference( ); } - return model; + return withLocalGatewayConnection(model); +} + +/** + * A gateway-routed SDK instance (`gateway("openai/gpt-5.6")`) authenticates + * from `AI_GATEWAY_API_KEY`/`VERCEL_OIDC_TOKEN` and never sees the connection + * `/login` saved. While such a connection is active, serve the same model id + * through it, as a string model would be. Outside `eve dev` the authored + * instance is returned untouched. + */ +function withLocalGatewayConnection(model: LanguageModel): LanguageModel { + if (typeof model === "string") return model; + if (typeof model.provider !== "string" || model.provider.split(".")[0] !== "gateway") { + return model; + } + if (typeof model.modelId !== "string" || model.modelId === "") return model; + return localGatewayModel(model.modelId) ?? model; } function isSourceBackedRuntimeModelReference(