Address Umans provider review feedback
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user