Address Umans provider review feedback

This commit is contained in:
oldschoola
2026-06-15 04:33:05 -07:00
parent 23e180d5cb
commit d7df0b6a09
10 changed files with 174 additions and 15 deletions
+1 -1
View File
@@ -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
+3 -4
View File
@@ -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,
@@ -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",
+69
View File
@@ -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);
});
});
+1 -1
View File
@@ -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
@@ -82,9 +82,11 @@ export function applyGeneratedModelPolicies(models: ModelSpec<Api>[]): void {
*/
export function rebakeModelThinking(model: ModelSpec<Api>): 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;
}
+2 -1
View File
@@ -69035,7 +69035,8 @@
"medium",
"high",
"xhigh"
]
],
"requiresEffort": true
}
},
"umans-qwen3.6-35b-a3b": {
@@ -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<string, Effort> = {
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 },
+33 -2
View File
@@ -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<string, Record<string, BundledModel>>;
const model = providers.umans?.["umans-kimi-k2.7"];
expect(model).toBeDefined();
expect(model.thinking).toMatchObject({
requiresEffort: true,
});
});
});
+1 -1
View File
@@ -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