diff --git a/changelog.d/fixes/11861-nous-tags-user-injection.md b/changelog.d/fixes/11861-nous-tags-user-injection.md new file mode 100644 index 0000000000..bfe9bc8464 --- /dev/null +++ b/changelog.d/fixes/11861-nous-tags-user-injection.md @@ -0,0 +1 @@ +- **fix(provider/nous):** inject required user= tag into Nous Research inference requests to resolve upstream 400 "missing tags" error ([#11861](https://github.com/diegosouzapw/OmniRoute/issues/11861)) — thanks @Karan825 diff --git a/open-sse/executors/default.ts b/open-sse/executors/default.ts index 0d2c9182bd..c8395e9d4a 100644 --- a/open-sse/executors/default.ts +++ b/open-sse/executors/default.ts @@ -639,8 +639,7 @@ export class DefaultExecutor extends BaseExecutor { const record = body as Record; const rf = record.response_format as - | { type?: string; json_schema?: { schema?: unknown } } - | undefined; + { type?: string; json_schema?: { schema?: unknown } } | undefined; if (!rf) return body; // openai-compatible-* providers accept json_object natively — only the @@ -748,6 +747,38 @@ export class DefaultExecutor extends BaseExecutor { delete withoutClientMetadata.client_metadata; withDefaults = withoutClientMetadata; } + // Nous Research inference gateway (portal.nousresearch.com) requires a top-level + // `tags` array containing at least a `user=` item on raw API-key requests (#11861). + // Without `tags`, upstream returns 400 "missing tags". + // Without `user=...`, upstream returns 400 "missing user tag". + if ( + this.provider === "nous-research" && + withDefaults && + typeof withDefaults === "object" && + !Array.isArray(withDefaults) + ) { + const record = withDefaults as Record; + const extraBody = record.extra_body as Record | undefined; + + const rawTags = Array.isArray(record.tags) + ? (record.tags as unknown[]) + : Array.isArray(extraBody?.tags) + ? (extraBody.tags as unknown[]) + : []; + + const stringTags = rawTags.filter( + (t): t is string => typeof t === "string" && t.trim().length > 0 + ); + + const hasUserTag = stringTags.some((t) => t.startsWith("user=")); + if (!hasUserTag) { + const username = + typeof record.user === "string" && record.user.trim() ? record.user.trim() : "omniroute"; + record.tags = [...stringTags, `user=${username}`]; + } else { + record.tags = stringTags; + } + } // 9router#1649: Mistral's API returns 422 (extra_forbidden) when an // assistant message carries a `reasoning_content` field (replayed thinking diff --git a/src/lib/providers/validation/audioMiscProviders.ts b/src/lib/providers/validation/audioMiscProviders.ts index c0976de088..eeb0f3c5eb 100644 --- a/src/lib/providers/validation/audioMiscProviders.ts +++ b/src/lib/providers/validation/audioMiscProviders.ts @@ -631,6 +631,7 @@ export async function validateNousResearchProvider({ apiKey, providerSpecificDat model: modelId, messages: [{ role: "user", content: "test" }], max_tokens: 1, + tags: ["user=omniroute"], }), }); diff --git a/tests/unit/executor-nous-research.test.ts b/tests/unit/executor-nous-research.test.ts index a20e9735c0..8694a055f7 100644 --- a/tests/unit/executor-nous-research.test.ts +++ b/tests/unit/executor-nous-research.test.ts @@ -29,8 +29,62 @@ test("nous-research DefaultExecutor.buildUrl() targets the correct inference end const url = executor.buildUrl("Hermes-4-70B", false, 0, null); - assert.equal( - url, - "https://inference-api.nousresearch.com/v1/chat/completions" - ); + assert.equal(url, "https://inference-api.nousresearch.com/v1/chat/completions"); +}); + +test("nous-research DefaultExecutor.transformRequest injects user=omniroute tag when tags is absent (#11861)", () => { + const executor = new DefaultExecutor("nous-research"); + const transformed = executor.transformRequest( + "Hermes-4-70B", + { model: "Hermes-4-70B", messages: [{ role: "user", content: "hello" }] }, + false, + null + ) as Record; + + assert.ok(Array.isArray(transformed.tags), "Expected tags array on nous-research body"); + assert.deepEqual(transformed.tags, ["user=omniroute"]); +}); + +test("nous-research DefaultExecutor.transformRequest respects client-sent user in tags (#11861)", () => { + const executor = new DefaultExecutor("nous-research"); + const transformed = executor.transformRequest( + "Hermes-4-70B", + { model: "Hermes-4-70B", user: "dev-user", messages: [{ role: "user", content: "hello" }] }, + false, + null + ) as Record; + + assert.deepEqual(transformed.tags, ["user=dev-user"]); +}); + +test("nous-research DefaultExecutor.transformRequest preserves existing tags and appends user tag (#11861)", () => { + const executor = new DefaultExecutor("nous-research"); + const transformed = executor.transformRequest( + "Hermes-4-70B", + { + model: "Hermes-4-70B", + tags: ["client=agent"], + messages: [{ role: "user", content: "hello" }], + }, + false, + null + ) as Record; + + assert.deepEqual(transformed.tags, ["client=agent", "user=omniroute"]); +}); + +test("nous-research DefaultExecutor.transformRequest does not duplicate user tag if already present (#11861)", () => { + const executor = new DefaultExecutor("nous-research"); + const transformed = executor.transformRequest( + "Hermes-4-70B", + { + model: "Hermes-4-70B", + tags: ["user=custom-tag"], + messages: [{ role: "user", content: "hello" }], + }, + false, + null + ) as Record; + + assert.deepEqual(transformed.tags, ["user=custom-tag"]); });