From d7df0b6a0962e4449aa40981e6cd2bbf0a92e79f Mon Sep 17 00:00:00 2001 From: oldschoola Date: Mon, 15 Jun 2026 04:33:05 -0700 Subject: [PATCH] Address Umans provider review feedback --- packages/ai/CHANGELOG.md | 2 +- packages/ai/src/providers/anthropic.ts | 7 +- packages/ai/test/anthropic-alignment.test.ts | 17 +++++ packages/ai/test/umans-login.test.ts | 69 +++++++++++++++++++ packages/catalog/CHANGELOG.md | 2 +- .../catalog/scripts/generated-policies.ts | 4 +- packages/catalog/src/models.json | 3 +- .../src/provider-models/openai-compat.ts | 48 +++++++++++-- packages/catalog/test/umans-provider.test.ts | 35 +++++++++- packages/coding-agent/CHANGELOG.md | 2 +- 10 files changed, 174 insertions(+), 15 deletions(-) create mode 100644 packages/ai/test/umans-login.test.ts diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 084a43a18..dcd941ed0 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added -- Added Umans AI Coding Plan API-key login support and `UMANS_AI_CODING_PLAN_API_KEY` environment fallback. +- Added Umans AI Coding Plan API-key login support and `UMANS_AI_CODING_PLAN_API_KEY` environment fallback ([#2636](https://github.com/can1357/oh-my-pi/pull/2636) by [@oldschoola](https://github.com/oldschoola)). ## [15.13.3] - 2026-06-15 diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index ba73c497a..60d82aa5e 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -2382,10 +2382,9 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A }; } - // OpenCode Go's Anthropic-compatible gateway validates API-key auth through - // `X-Api-Key`; bearer-only requests reach the endpoint but return - // `Missing API key` before token validation. - if (model.provider === "opencode-go") { + // OpenCode Go and Umans validate Anthropic-compatible API-key auth through + // `X-Api-Key`; bearer-only requests reach the endpoint but fail auth. + if (model.provider === "opencode-go" || model.provider === "umans") { delete defaultHeaders.Authorization; return { isOAuthToken: false, diff --git a/packages/ai/test/anthropic-alignment.test.ts b/packages/ai/test/anthropic-alignment.test.ts index 924d3211f..827be6651 100644 --- a/packages/ai/test/anthropic-alignment.test.ts +++ b/packages/ai/test/anthropic-alignment.test.ts @@ -452,6 +452,23 @@ describe("Anthropic request fingerprint alignment", () => { expect(headers["X-Api-Key"]).toBeUndefined(); }); + it("keeps Umans Anthropic-compatible requests on X-Api-Key auth", () => { + const options = buildAnthropicClientOptions({ + model: buildModel({ + ...ANTHROPIC_MODEL_SPEC, + id: "umans-coder", + name: "Umans Coder", + provider: "umans", + baseUrl: "https://api.code.umans.ai", + }), + apiKey: "sk-umans-test", + stream: true, + }); + + expect(options.apiKey).toBe("sk-umans-test"); + expect(options.defaultHeaders.Authorization).toBeUndefined(); + }); + it("forwards only prefix-matching Claude Code User-Agent values", () => { const forwardedHeaders = buildAnthropicHeaders({ apiKey: "sk-ant-oat-test", diff --git a/packages/ai/test/umans-login.test.ts b/packages/ai/test/umans-login.test.ts new file mode 100644 index 000000000..955e62772 --- /dev/null +++ b/packages/ai/test/umans-login.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it, vi } from "bun:test"; +import { loginUmans } from "@oh-my-pi/pi-ai/registry/umans"; +import type { FetchImpl } from "@oh-my-pi/pi-ai/types"; + +describe("umans login", () => { + it("validates pasted keys against the Anthropic messages endpoint", async () => { + let authUrl: string | undefined; + let authInstructions: string | undefined; + let promptMessage: string | undefined; + let promptPlaceholder: string | undefined; + const fetchMock: FetchImpl = vi.fn(async (input: string | URL | Request, init?: RequestInit) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const headers = new Headers(init?.headers); + const body = JSON.parse(String(init?.body)) as { model?: string; max_tokens?: number }; + + expect(url).toBe("https://api.code.umans.ai/v1/messages"); + expect(init?.method).toBe("POST"); + expect(headers.get("content-type")).toBe("application/json"); + expect(headers.get("anthropic-version")).toBe("2023-06-01"); + expect(headers.get("x-api-key")).toBe("sk-umans-valid"); + expect(headers.get("authorization")).toBeNull(); + expect(body.model).toBe("umans-coder"); + expect(body.max_tokens).toBe(1); + + return new Response(JSON.stringify({ id: "msg_test", type: "message" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }); + + const apiKey = await loginUmans({ + onAuth: info => { + authUrl = info.url; + authInstructions = info.instructions; + }, + onPrompt: async prompt => { + promptMessage = prompt.message; + promptPlaceholder = prompt.placeholder; + return " sk-umans-valid "; + }, + fetch: fetchMock, + }); + + expect(apiKey).toBe("sk-umans-valid"); + expect(authUrl).toBe("https://app.umans.ai/billing"); + expect(authInstructions).toContain("Dashboard → API Keys"); + expect(promptMessage).toBe("Paste your Umans API key"); + expect(promptPlaceholder).toBe("sk-..."); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + it("surfaces validation errors from the Anthropic messages endpoint", async () => { + const fetchMock: FetchImpl = vi.fn( + async () => + new Response("invalid key", { + status: 401, + headers: { "Content-Type": "text/plain" }, + }), + ); + + await expect( + loginUmans({ + onPrompt: async () => "sk-umans-bad", + fetch: fetchMock, + }), + ).rejects.toThrow("Umans AI Coding Plan API key validation failed (401): invalid key"); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/catalog/CHANGELOG.md b/packages/catalog/CHANGELOG.md index 693c46678..231881224 100644 --- a/packages/catalog/CHANGELOG.md +++ b/packages/catalog/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added -- Added the Umans AI Coding Plan provider catalog with Anthropic-compatible model metadata and dynamic discovery. +- Added the Umans AI Coding Plan provider catalog with Anthropic-compatible model metadata and dynamic discovery ([#2636](https://github.com/can1357/oh-my-pi/pull/2636) by [@oldschoola](https://github.com/oldschoola)). ## [15.13.3] - 2026-06-15 diff --git a/packages/catalog/scripts/generated-policies.ts b/packages/catalog/scripts/generated-policies.ts index ba6fbdbf6..330ff4ae4 100644 --- a/packages/catalog/scripts/generated-policies.ts +++ b/packages/catalog/scripts/generated-policies.ts @@ -82,9 +82,11 @@ export function applyGeneratedModelPolicies(models: ModelSpec[]): void { */ export function rebakeModelThinking(model: ModelSpec): void { if (isVariantCollapsedSpec(model)) return; + const requiresProviderAuthoredEffort = + model.provider === "umans" && (model.thinking?.requiresEffort === true || model.id === "umans-kimi-k2.7"); const thinking = resolveModelThinking({ ...model, thinking: undefined }, buildCompat(model)); if (thinking) { - model.thinking = thinking; + model.thinking = requiresProviderAuthoredEffort ? { ...thinking, requiresEffort: true } : thinking; } else { delete model.thinking; } diff --git a/packages/catalog/src/models.json b/packages/catalog/src/models.json index ed1c15ddd..62269746c 100644 --- a/packages/catalog/src/models.json +++ b/packages/catalog/src/models.json @@ -69035,7 +69035,8 @@ "medium", "high", "xhigh" - ] + ], + "requiresEffort": true } }, "umans-qwen3.6-35b-a3b": { diff --git a/packages/catalog/src/provider-models/openai-compat.ts b/packages/catalog/src/provider-models/openai-compat.ts index a24d3c6a2..6a23c88a4 100644 --- a/packages/catalog/src/provider-models/openai-compat.ts +++ b/packages/catalog/src/provider-models/openai-compat.ts @@ -574,6 +574,14 @@ function createSimpleAnthropicProviderOptions( const UMANS_BASE_URL = "https://api.code.umans.ai"; const UMANS_MODELS_INFO_PATH = "/models/info"; +const UMANS_REASONING_EFFORT_BY_LEVEL: Record = { + minimal: Effort.Minimal, + low: Effort.Low, + medium: Effort.Medium, + high: Effort.High, + xhigh: Effort.XHigh, +}; +const UMANS_DEFAULT_REASONING_EFFORTS = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh] as const; export interface UmansModelManagerConfig { apiKey?: string; @@ -597,10 +605,40 @@ function umansSupportsVision(value: unknown): boolean { } function umansReasoningSupported(value: unknown): boolean { - if (isRecord(value)) { - return value.supported === true; + return isRecord(value) ? value.supported === true : value === true; +} + +function mapUmansReasoningEfforts(value: unknown): readonly Effort[] { + if (!isRecord(value) || !Array.isArray(value.levels)) { + return UMANS_DEFAULT_REASONING_EFFORTS; } - return value === true; + const efforts: Effort[] = []; + for (const level of value.levels) { + if (typeof level !== "string") continue; + const effort = UMANS_REASONING_EFFORT_BY_LEVEL[level]; + if (effort !== undefined && !efforts.includes(effort)) { + efforts.push(effort); + } + } + return efforts.length > 0 ? efforts : UMANS_DEFAULT_REASONING_EFFORTS; +} + +function mapUmansThinkingConfig(value: unknown): ThinkingConfig | undefined { + if (!umansReasoningSupported(value)) return undefined; + const efforts = mapUmansReasoningEfforts(value); + const thinking: ThinkingConfig = { mode: "budget", efforts }; + if (isRecord(value)) { + if (value.can_disable === false) { + thinking.requiresEffort = true; + } + if (typeof value.default_level === "string") { + const defaultLevel = UMANS_REASONING_EFFORT_BY_LEVEL[value.default_level]; + if (defaultLevel !== undefined && efforts.includes(defaultLevel)) { + thinking.defaultLevel = defaultLevel; + } + } + } + return thinking; } function mapUmansModelInfo( @@ -612,6 +650,7 @@ function mapUmansModelInfo( if (!modelId) return null; const capabilities = isRecord(raw.capabilities) ? raw.capabilities : {}; const supportsTools = capabilities.supports_tools; + const thinking = mapUmansThinkingConfig(capabilities.reasoning); return { ...reference, id: modelId, @@ -619,7 +658,8 @@ function mapUmansModelInfo( api: "anthropic-messages", provider: "umans", baseUrl, - reasoning: umansReasoningSupported(capabilities.reasoning), + reasoning: thinking !== undefined, + ...(thinking ? { thinking } : {}), input: umansSupportsVision(capabilities.supports_vision) ? ["text", "image"] : ["text"], ...(supportsTools === false ? { supportsTools: false } : {}), cost: reference?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, diff --git a/packages/catalog/test/umans-provider.test.ts b/packages/catalog/test/umans-provider.test.ts index 470cd35b0..ec5818cfd 100644 --- a/packages/catalog/test/umans-provider.test.ts +++ b/packages/catalog/test/umans-provider.test.ts @@ -15,6 +15,10 @@ interface BundledModel { input: string[]; contextWindow: number | null; maxTokens: number | null; + thinking?: { + defaultLevel?: string; + requiresEffort?: boolean; + }; } describe("umans provider catalog", () => { @@ -31,7 +35,17 @@ describe("umans provider catalog", () => { max_completion_tokens: 262_144, supports_vision: true, supports_tools: true, - reasoning: { supported: true }, + reasoning: { supported: true, can_disable: true, default_level: "medium" }, + }, + }, + "umans-kimi-k2.7": { + display_name: "Umans Kimi K2.7 Code", + capabilities: { + context_window: 262_144, + max_completion_tokens: 262_144, + supports_vision: true, + supports_tools: true, + reasoning: { supported: true, can_disable: false, default_level: "medium" }, }, }, }), @@ -47,7 +61,7 @@ describe("umans provider catalog", () => { expect(requestedUrls).toEqual(["https://api.code.umans.ai/v1/models/info"]); expect(models).not.toBeNull(); - const model = models?.[0]; + const model = models?.find(item => item.id === "umans-coder"); expect(model).toMatchObject({ id: "umans-coder", name: "Umans Coder", @@ -58,6 +72,13 @@ describe("umans provider catalog", () => { input: ["text", "image"], contextWindow: 262_144, maxTokens: 262_144, + thinking: { defaultLevel: "medium" }, + }); + const mandatoryReasoningModel = models?.find(item => item.id === "umans-kimi-k2.7"); + expect(mandatoryReasoningModel).toMatchObject({ + id: "umans-kimi-k2.7", + reasoning: true, + thinking: { defaultLevel: "medium", requiresEffort: true }, }); }); @@ -108,4 +129,14 @@ describe("umans provider catalog", () => { maxTokens: 262_144, }); }); + + it("bundles Umans mandatory reasoning metadata", () => { + const providers = modelsJson as Record>; + const model = providers.umans?.["umans-kimi-k2.7"]; + + expect(model).toBeDefined(); + expect(model.thinking).toMatchObject({ + requiresEffort: true, + }); + }); }); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e1827e62d..9b2c627a6 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added -- Added `UMANS_AI_CODING_PLAN_API_KEY` to the CLI environment help. +- Added `UMANS_AI_CODING_PLAN_API_KEY` to the CLI environment help ([#2636](https://github.com/can1357/oh-my-pi/pull/2636) by [@oldschoola](https://github.com/oldschoola)). ## [15.13.3] - 2026-06-15