mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
This commit is contained in:
committed by
GitHub
parent
c0682c1ac2
commit
a6dfd1068e
1
changelog.d/fixes/6142-6142-devin-unified-api.md
Normal file
1
changelog.d/fixes/6142-6142-devin-unified-api.md
Normal 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)
|
||||
@@ -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)" },
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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() : "";
|
||||
|
||||
71
tests/unit/devin-cloud-agent-validator-6142.test.ts
Normal file
71
tests/unit/devin-cloud-agent-validator-6142.test.ts
Normal 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");
|
||||
});
|
||||
27
tests/unit/repro-6142-devin-cloud-agent-unwired.test.ts
Normal file
27
tests/unit/repro-6142-devin-cloud-agent-unwired.test.ts
Normal 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"
|
||||
);
|
||||
});
|
||||
@@ -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}`);
|
||||
|
||||
Reference in New Issue
Block a user