fix(providers): wire devin cloud-agent into provider validation and static models (#6142) (#6894)

This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-12 01:59:53 -03:00
committed by GitHub
parent c0682c1ac2
commit a6dfd1068e
7 changed files with 147 additions and 3 deletions

View File

@@ -0,0 +1 @@
- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142)

View File

@@ -79,6 +79,12 @@ const STATIC_MODEL_PROVIDERS: Record<string, () => Array<{ id: string; name: str
// Google Labs async coding agent — single async session, no model selection.
{ id: "jules", name: "Jules (Google Labs coding agent)" },
],
devin: () => [
// Cognition's Devin cloud-agent sessions don't expose per-request model
// selection like devin-cli's ACP models do — single non-selectable placeholder
// so the "Available Models" UI shows something instead of a hard failure (#6142).
{ id: "devin", name: "Devin (Cognition cloud agent)" },
],
"linkup-search": () => [
// Linkup web search — the "model" is the search depth (docs.linkup.so #5571).
{ id: "standard", name: "Standard (single-iteration agentic search)" },

View File

@@ -45,6 +45,7 @@ import {
validateCopilotWebProvider,
validateT3WebProvider,
validateJulesProvider,
validateDevinCloudAgentProvider,
validateInnerAiProvider,
} from "./validation/webProvidersB";
import {
@@ -390,6 +391,10 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi
}
},
jules: validateJulesProvider,
// "devin" is the Cognition cloud-agent provider (distinct from the "devin-cli"
// LLM/ACP provider, which is already registered in providerRegistry). Wired here
// for parity with the "jules" cloud-agent entry above — see #6142.
devin: validateDevinCloudAgentProvider,
// auggie is a fully local, credential-less CLI passthrough — there is no API
// key to check upstream. The only meaningful validation is confirming the
// `auggie` binary is installed and runnable on this machine.

View File

@@ -1,6 +1,7 @@
// Web-cookie provider key validators (part B): muse-spark-web, adapta-web, claude-web, gemini-web,
// copilot-web, t3-web, jules, inner-ai. Extracted from validation.ts (god-file decomposition) —
// top-level functions with no dispatcher-state captures; behavior is byte-identical to the inline defs.
// copilot-web, t3-web, jules, devin (cloud-agent), inner-ai. Extracted from validation.ts (god-file
// decomposition) — top-level functions with no dispatcher-state captures; behavior is byte-identical
// to the inline defs.
import { applyCustomUserAgent } from "./headers";
import { toValidationErrorResult, validationRead, validationWrite } from "./transport";
import { normalizeSessionCookieHeader } from "@/lib/providers/webCookieAuth";
@@ -453,6 +454,38 @@ export async function validateJulesProvider({ apiKey }: { apiKey: string }) {
}
}
/**
* Devin cloud-agent (Cognition) — GET /v1/sessions with Bearer auth
* (see docs.devin.ai/api-reference/sessions/list-sessions). Distinct from the
* "devin-cli" LLM provider (ACP), which is already wired via providerRegistry.
*/
export async function validateDevinCloudAgentProvider({ apiKey }: { apiKey: string }) {
try {
const response = await validationWrite("https://api.devin.ai/v1/sessions?limit=1", {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
if (response.status === 401 || response.status === 403) {
return { valid: false, error: "Invalid API key" };
}
if (response.ok) {
return { valid: true, error: null };
}
const errorText = await response.text().catch(() => "");
return {
valid: false,
error: errorText.trim() || `Devin API returned ${response.status}`,
};
} catch (error: unknown) {
return toValidationErrorResult(error);
}
}
export async function validateInnerAiProvider({ apiKey, providerSpecificData = {} }: any) {
try {
const raw = typeof apiKey === "string" ? apiKey.trim() : "";

View File

@@ -0,0 +1,71 @@
// #6142 — devin cloud-agent provider validator + static model catalog wiring.
// Regression guard for the "not supported" fallback devin used to always hit on the
// generic Providers config page (parity with the existing jules cloud-agent wiring).
import { test } from "node:test";
import assert from "node:assert/strict";
import { validateProviderApiKey } from "../../src/lib/providers/validation.ts";
import { validateDevinCloudAgentProvider } from "../../src/lib/providers/validation/webProvidersB.ts";
import { getStaticModelsForProvider } from "../../src/lib/providers/staticModels.ts";
test("#6142: devin cloud-agent validator is wired into the SPECIALTY_VALIDATORS dispatcher", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () =>
new Response("{}", { status: 401 })) as unknown as typeof fetch;
try {
const result = await validateProviderApiKey({
provider: "devin",
apiKey: "cog_bad_key",
});
assert.equal(result.valid, false);
assert.equal(result.error, "Invalid API key");
assert.notEqual(result.unsupported, true);
} finally {
globalThis.fetch = originalFetch;
}
});
test("#6142: validateDevinCloudAgentProvider maps 401 to Invalid API key", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () =>
new Response("{}", { status: 401 })) as unknown as typeof fetch;
try {
const result = await validateDevinCloudAgentProvider({ apiKey: "bad-key" });
assert.equal(result.valid, false);
assert.equal(result.error, "Invalid API key");
} finally {
globalThis.fetch = originalFetch;
}
});
test("#6142: validateDevinCloudAgentProvider maps 403 to Invalid API key", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () =>
new Response("{}", { status: 403 })) as unknown as typeof fetch;
try {
const result = await validateDevinCloudAgentProvider({ apiKey: "bad-key" });
assert.equal(result.valid, false);
assert.equal(result.error, "Invalid API key");
} finally {
globalThis.fetch = originalFetch;
}
});
test("#6142: validateDevinCloudAgentProvider accepts a 2xx probe as valid", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () =>
new Response(JSON.stringify({ sessions: [] }), { status: 200 })) as unknown as typeof fetch;
try {
const result = await validateDevinCloudAgentProvider({ apiKey: "cog_good_key" });
assert.equal(result.valid, true);
assert.equal(result.error, null);
} finally {
globalThis.fetch = originalFetch;
}
});
test("#6142: devin exposes a static model catalog for the 'Available Models' UI (parity with jules)", () => {
const models = getStaticModelsForProvider("devin");
assert.ok(Array.isArray(models) && models.length > 0);
assert.equal(models[0].id, "devin");
});

View File

@@ -0,0 +1,27 @@
// tests/unit/repro-6142-devin-cloud-agent-unwired.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { validateProviderApiKey } from "../../src/lib/providers/validation";
import { getStaticModelsForProvider } from "../../src/lib/providers/staticModels";
test("#6142 (fixed): saving a Devin cloud-agent API key should not be 'unsupported' by the generic provider flow (parity with jules)", async () => {
const result = await validateProviderApiKey({
provider: "devin",
apiKey: "cog_fake_service_user_token_for_repro",
});
assert.notEqual(
result.error,
"Provider validation not supported",
"devin should have a specialty validator wired (like jules), not fall through to 'not supported'"
);
assert.notEqual(result.unsupported, true);
});
test("#6142 (fixed): the 'Available Models' UI should have a usable static catalog for devin (parity with jules)", () => {
const devinStaticModels = getStaticModelsForProvider("devin");
assert.ok(
Array.isArray(devinStaticModels) && devinStaticModels.length > 0,
"devin should expose a static model catalog like jules does"
);
});

View File

@@ -25,7 +25,7 @@ test("webProvidersA exposes its six validators (deepseek/qwen/grok/chatgpt/perpl
}
});
test("webProvidersB exposes its eight validators (muse-spark/adapta/claude/gemini/copilot/t3/jules/inner-ai)", () => {
test("webProvidersB exposes its nine validators (muse-spark/adapta/claude/gemini/copilot/t3/jules/devin/inner-ai)", () => {
for (const name of [
"validateMuseSparkWebProvider",
"validateAdaptaWebProvider",
@@ -34,6 +34,7 @@ test("webProvidersB exposes its eight validators (muse-spark/adapta/claude/gemin
"validateCopilotWebProvider",
"validateT3WebProvider",
"validateJulesProvider",
"validateDevinCloudAgentProvider",
"validateInnerAiProvider",
]) {
assert.equal(typeof (B as Record<string, unknown>)[name], "function", `B missing ${name}`);