feat: introduced aiand provider registry entry and model catalog
- Implement the ai& provider registry entry with API-key authentication and login support. - Add model descriptors, static model seeding, and openai-compatible model discovery for the ai& provider. - Update the model catalog with ai& provider models, pricing, and updated provider model names. - Add unit tests for the ai& provider environment resolution, metadata, and dynamic model mapping.
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added the ai& (`aiand`) provider registry entry with API-key paste login validated against `https://api.aiand.com/v1/models`.
|
||||
|
||||
## [17.2.2] - 2026-07-31
|
||||
|
||||
### Added
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createApiKeyLogin } from "./api-key-login";
|
||||
import type { OAuthLoginCallbacks } from "./oauth/types";
|
||||
import type { ProviderDefinition } from "./types";
|
||||
|
||||
export const loginAiand = createApiKeyLogin({
|
||||
providerLabel: "ai&",
|
||||
authUrl: "https://console.aiand.com/api-keys",
|
||||
instructions: "Copy your API key from the ai& console",
|
||||
promptMessage: "Paste your ai& API key",
|
||||
placeholder: "sk-...",
|
||||
validation: {
|
||||
kind: "models-endpoint",
|
||||
provider: "ai&",
|
||||
modelsUrl: "https://api.aiand.com/v1/models",
|
||||
},
|
||||
});
|
||||
|
||||
export const aiandProvider = {
|
||||
id: "aiand",
|
||||
name: "ai&",
|
||||
login: (cb: OAuthLoginCallbacks) => loginAiand(cb),
|
||||
} as const satisfies ProviderDefinition;
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { KnownProvider } from "@oh-my-pi/pi-catalog";
|
||||
import { aiandProvider } from "./aiand";
|
||||
import { aimlApiProvider } from "./aimlapi";
|
||||
import { alibabaCodingPlanProvider } from "./alibaba-coding-plan";
|
||||
import { alibabaTokenPlanProvider } from "./alibaba-token-plan";
|
||||
@@ -101,6 +102,7 @@ const ALL = [
|
||||
gitLabDuoWorkflowProvider,
|
||||
alibabaCodingPlanProvider,
|
||||
alibabaTokenPlanProvider,
|
||||
aiandProvider,
|
||||
aimlApiProvider,
|
||||
zhipuCodingPlanProvider,
|
||||
umansProvider,
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for the ai& provider (`aiand`), an OpenAI-compatible inference API with dynamic model discovery (context windows, capabilities, reasoning efforts, and USD pricing from `/v1/models`) and API-key authentication via the `AIAND_API_KEY` environment variable.
|
||||
|
||||
## [17.2.2] - 2026-07-31
|
||||
|
||||
### Added
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
} from "../src/provider-models/descriptor-types";
|
||||
import { PROVIDER_DESCRIPTORS } from "../src/provider-models/descriptors";
|
||||
import {
|
||||
AIAND_STATIC_MODELS,
|
||||
ALIBABA_TOKEN_PLAN_STATIC_MODELS,
|
||||
ANTHROPIC_CURATED_FALLBACK_MODELS,
|
||||
buildFireworksFastSeed,
|
||||
@@ -560,6 +561,12 @@ async function generateModels() {
|
||||
if (!authoritativeCatalogProviders.has("sakana")) {
|
||||
allModels.push(...SAKANA_FUGU_STATIC_MODELS);
|
||||
}
|
||||
// Seed ai&'s documented catalog so the provider is usable when generation
|
||||
// has no AIAND_API_KEY. A live org-scoped `/v1/models` snapshot is
|
||||
// authoritative and replaces the seed.
|
||||
if (!authoritativeCatalogProviders.has("aiand")) {
|
||||
allModels.push(...AIAND_STATIC_MODELS);
|
||||
}
|
||||
// Seed the GMI Cloud default model so a fresh install (and a regen without a
|
||||
// `GMI_API_KEY`) still resolves the descriptor's `defaultModel` synchronously
|
||||
// at boot. If live `/v1/models` discovery succeeds, it is authoritative.
|
||||
|
||||
+623
-112
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ import type { ModelManagerConfig, ProviderCatalogEntry, ProviderDescriptor } fro
|
||||
import { googleModelManagerOptions, googleVertexModelManagerOptions } from "./google";
|
||||
import { ollamaCloudModelManagerOptions } from "./ollama";
|
||||
import {
|
||||
aiandModelManagerOptions,
|
||||
aimlApiModelManagerOptions,
|
||||
alibabaCodingPlanModelManagerOptions,
|
||||
alibabaTokenPlanModelManagerOptions,
|
||||
@@ -65,6 +66,14 @@ import {
|
||||
} from "./special";
|
||||
|
||||
export const CATALOG_PROVIDERS = [
|
||||
{
|
||||
id: "aiand",
|
||||
defaultModel: "moonshotai/kimi-k2.7-code",
|
||||
envVars: ["AIAND_API_KEY"],
|
||||
createModelManagerOptions: (config: ModelManagerConfig) => aiandModelManagerOptions(config),
|
||||
dynamicModelsAuthoritative: true,
|
||||
catalogDiscovery: { label: "ai&" },
|
||||
},
|
||||
{
|
||||
id: "aimlapi",
|
||||
defaultModel: "gpt-5.5-2026-04-23",
|
||||
|
||||
@@ -3796,6 +3796,175 @@ export function sakanaModelManagerOptions(config?: SakanaModelManagerConfig): Mo
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 16.6 ai& (aiand.com)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const AIAND_DEFAULT_BASE_URL = "https://api.aiand.com/v1";
|
||||
|
||||
/** `reasoning_efforts` wire values ai& reports, mapped onto pi effort levels. */
|
||||
const AIAND_EFFORT_BY_WIRE_VALUE: Record<string, Effort> = {
|
||||
minimal: Effort.Minimal,
|
||||
low: Effort.Low,
|
||||
medium: Effort.Medium,
|
||||
high: Effort.High,
|
||||
xhigh: Effort.XHigh,
|
||||
max: Effort.Max,
|
||||
};
|
||||
|
||||
function normalizeAiandBaseUrl(baseUrl: string | undefined): string {
|
||||
const value = baseUrl?.trim() || AIAND_DEFAULT_BASE_URL;
|
||||
const normalized = value.replace(/\/+$/, "");
|
||||
return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
|
||||
}
|
||||
|
||||
function createAiandStaticModel(
|
||||
id: string,
|
||||
name: string,
|
||||
cost: { input: number; output: number },
|
||||
contextWindow: number,
|
||||
input: ModelSpec<"openai-completions">["input"],
|
||||
): ModelSpec<"openai-completions"> {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
api: "openai-completions",
|
||||
provider: "aiand",
|
||||
baseUrl: AIAND_DEFAULT_BASE_URL,
|
||||
reasoning: true,
|
||||
input: [...input],
|
||||
cost: { input: cost.input, output: cost.output, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow,
|
||||
maxTokens: null,
|
||||
thinking: { mode: "effort", efforts: [Effort.Low, Effort.Medium, Effort.High], defaultLevel: Effort.Medium },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Documented ai& catalog (docs.aiand.com/models/catalog, 2026-08) bundled so
|
||||
* the provider is usable when generation and first boot have no live key.
|
||||
* The org-scoped `/v1/models` response is authoritative once discovery runs.
|
||||
*/
|
||||
export const AIAND_STATIC_MODELS: readonly ModelSpec<"openai-completions">[] = [
|
||||
createAiandStaticModel("qwen/qwen3.6-27b", "Qwen3.6 27B", { input: 0, output: 0 }, 262_144, ["text"]),
|
||||
createAiandStaticModel(
|
||||
"deepseek-ai/deepseek-v4-flash",
|
||||
"DeepSeek V4 Flash",
|
||||
{ input: 0.15, output: 0.25 },
|
||||
1_000_000,
|
||||
["text"],
|
||||
),
|
||||
createAiandStaticModel("google/gemma-4-31b-it", "Gemma 4 31B IT", { input: 0.2, output: 0.5 }, 262_144, [
|
||||
"text",
|
||||
"image",
|
||||
]),
|
||||
createAiandStaticModel("openai/gpt-oss-120b", "GPT OSS 120B", { input: 0.15, output: 0.6 }, 131_072, ["text"]),
|
||||
createAiandStaticModel("deepseek-ai/deepseek-v4-pro", "DeepSeek V4 Pro", { input: 1, output: 2.5 }, 1_000_000, [
|
||||
"text",
|
||||
]),
|
||||
createAiandStaticModel("moonshotai/kimi-k2.7-code", "Kimi K2.7 Code", { input: 0.75, output: 3.5 }, 262_144, [
|
||||
"text",
|
||||
"image",
|
||||
]),
|
||||
createAiandStaticModel("moonshotai/kimi-k2.6", "Kimi K2.6", { input: 0.85, output: 3.5 }, 262_144, [
|
||||
"text",
|
||||
"image",
|
||||
]),
|
||||
createAiandStaticModel("zai-org/glm-5.2", "GLM 5.2", { input: 1, output: 4 }, 1_000_000, ["text"]),
|
||||
createAiandStaticModel("zai-org/glm-5.1", "GLM 5.1", { input: 1.4, output: 4.4 }, 202_752, ["text"]),
|
||||
];
|
||||
|
||||
const AIAND_STATIC_MODEL_IDS = AIAND_STATIC_MODELS.map(model => model.id);
|
||||
|
||||
function mapAiandThinking(entry: OpenAICompatibleModelRecord): ThinkingConfig | undefined {
|
||||
const efforts = Array.isArray(entry.reasoning_efforts)
|
||||
? entry.reasoning_efforts.flatMap(value =>
|
||||
typeof value === "string" && AIAND_EFFORT_BY_WIRE_VALUE[value] ? [AIAND_EFFORT_BY_WIRE_VALUE[value]] : [],
|
||||
)
|
||||
: [];
|
||||
if (efforts.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const defaultLevel =
|
||||
typeof entry.reasoning_effort_default === "string"
|
||||
? AIAND_EFFORT_BY_WIRE_VALUE[entry.reasoning_effort_default]
|
||||
: undefined;
|
||||
return {
|
||||
mode: "effort",
|
||||
efforts,
|
||||
...(defaultLevel && efforts.includes(defaultLevel) && { defaultLevel }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* ai& reports prices as decimal strings per 1M tokens in the org's billing
|
||||
* currency (`usd` or `jpy`). Costs are only mapped for USD orgs — JPY figures
|
||||
* would corrupt the USD-denominated cost model, so they fall back to zero.
|
||||
*/
|
||||
function mapAiandCost(entry: OpenAICompatibleModelRecord): ModelSpec<"openai-completions">["cost"] {
|
||||
if (typeof entry.currency === "string" && entry.currency !== "usd") {
|
||||
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
||||
}
|
||||
return {
|
||||
input: toPositiveNumber(entry.input_per_1m, 0),
|
||||
output: toPositiveNumber(entry.output_per_1m, 0),
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function mapAiandModel(
|
||||
entry: OpenAICompatibleModelRecord,
|
||||
defaults: ModelSpec<"openai-completions">,
|
||||
): ModelSpec<"openai-completions"> {
|
||||
const capabilities: unknown[] = Array.isArray(entry.capabilities) ? entry.capabilities : [];
|
||||
const reasoning = capabilities.includes("reasoning");
|
||||
const thinking = reasoning ? mapAiandThinking(entry) : undefined;
|
||||
const description =
|
||||
typeof entry.description === "string" && entry.description.trim() ? entry.description : undefined;
|
||||
return {
|
||||
...defaults,
|
||||
name: description ?? toModelName(entry.name, defaults.name),
|
||||
reasoning,
|
||||
input: capabilities.includes("vision") ? ["text", "image"] : ["text"],
|
||||
cost: mapAiandCost(entry),
|
||||
contextWindow: toPositiveNumber(entry.context_window, null),
|
||||
...(thinking && { thinking }),
|
||||
};
|
||||
}
|
||||
|
||||
export interface AiandModelManagerConfig {
|
||||
apiKey?: string;
|
||||
baseUrl?: string;
|
||||
fetch?: FetchImpl;
|
||||
}
|
||||
|
||||
/**
|
||||
* ai& (aiand.com) model manager: OpenAI-compatible chat completions with an
|
||||
* org-scoped `/v1/models` catalog carrying context, capability, effort, and
|
||||
* pricing metadata, so discovery is authoritative over the bundled seed.
|
||||
*/
|
||||
export function aiandModelManagerOptions(config?: AiandModelManagerConfig): ModelManagerOptions<"openai-completions"> {
|
||||
const apiKey = config?.apiKey;
|
||||
const baseUrl = normalizeAiandBaseUrl(config?.baseUrl ?? Bun.env.AIAND_BASE_URL);
|
||||
return {
|
||||
providerId: "aiand",
|
||||
dynamicModelsAuthoritative: true,
|
||||
dropCachedModelIdsOnStaticMismatch: AIAND_STATIC_MODEL_IDS,
|
||||
...(apiKey && {
|
||||
fetchDynamicModels: () =>
|
||||
fetchOpenAICompatibleModels({
|
||||
api: "openai-completions",
|
||||
provider: "aiand",
|
||||
baseUrl,
|
||||
apiKey,
|
||||
mapModel: (entry, defaults) => mapAiandModel(entry, defaults),
|
||||
fetch: config?.fetch,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 17. Qwen Portal
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import { afterEach, describe, expect, test, vi } from "bun:test";
|
||||
import { getOAuthProviders } from "@oh-my-pi/pi-ai/registry/oauth";
|
||||
import { getEnvApiKey } from "@oh-my-pi/pi-ai/stream";
|
||||
import { Effort } from "@oh-my-pi/pi-catalog/effort";
|
||||
import { getBundledModels } from "@oh-my-pi/pi-catalog/models";
|
||||
import { DEFAULT_MODEL_PER_PROVIDER, PROVIDER_DESCRIPTORS } from "@oh-my-pi/pi-catalog/provider-models/descriptors";
|
||||
import { aiandModelManagerOptions } from "@oh-my-pi/pi-catalog/provider-models/openai-compat";
|
||||
import type { FetchImpl } from "@oh-my-pi/pi-catalog/types";
|
||||
|
||||
const ORIGINAL_ENV = {
|
||||
AIAND_API_KEY: Bun.env.AIAND_API_KEY,
|
||||
AIAND_BASE_URL: Bun.env.AIAND_BASE_URL,
|
||||
} as const;
|
||||
|
||||
function restoreEnvVar(name: keyof typeof ORIGINAL_ENV): void {
|
||||
const value = ORIGINAL_ENV[name];
|
||||
if (value === undefined) {
|
||||
delete Bun.env[name];
|
||||
return;
|
||||
}
|
||||
Bun.env[name] = value;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
restoreEnvVar("AIAND_API_KEY");
|
||||
restoreEnvVar("AIAND_BASE_URL");
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
/** One entry in ai&'s documented `/v1/models` OpenAI-surface response shape. */
|
||||
function aiandModelsResponse(entries: Record<string, unknown>[]): Response {
|
||||
return new Response(JSON.stringify({ object: "list", data: entries }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
describe("ai& provider support", () => {
|
||||
test("resolves the AIAND_API_KEY environment fallback", () => {
|
||||
Bun.env.AIAND_API_KEY = "aiand-test-key";
|
||||
expect(getEnvApiKey("aiand")).toBe("aiand-test-key");
|
||||
});
|
||||
|
||||
test("registers descriptor, default model, bundled seed, and login provider", () => {
|
||||
const descriptor = PROVIDER_DESCRIPTORS.find(item => item.providerId === "aiand");
|
||||
expect(descriptor).toBeDefined();
|
||||
expect(descriptor?.defaultModel).toBe("moonshotai/kimi-k2.7-code");
|
||||
expect(descriptor?.dynamicModelsAuthoritative).toBe(true);
|
||||
expect(DEFAULT_MODEL_PER_PROVIDER.aiand).toBe("moonshotai/kimi-k2.7-code");
|
||||
|
||||
const bundled = getBundledModels("aiand");
|
||||
const defaultModel = bundled.find(model => model.id === "moonshotai/kimi-k2.7-code");
|
||||
expect(defaultModel).toBeDefined();
|
||||
for (const model of bundled) {
|
||||
expect(model.api).toBe("openai-completions");
|
||||
expect(model.baseUrl).toBe("https://api.aiand.com/v1");
|
||||
}
|
||||
|
||||
const provider = getOAuthProviders().find(item => item.id === "aiand");
|
||||
expect(provider?.name).toBe("ai&");
|
||||
});
|
||||
|
||||
test("maps ai& /v1/models metadata: context, capabilities, efforts, and USD pricing", async () => {
|
||||
delete Bun.env.AIAND_BASE_URL;
|
||||
const fetchMock: FetchImpl = vi.fn(async () =>
|
||||
aiandModelsResponse([
|
||||
{
|
||||
id: "openai/gpt-oss-120b",
|
||||
name: "openai/gpt-oss-120b",
|
||||
description: "OpenAI GPT OSS 120B",
|
||||
context_window: 131072,
|
||||
capabilities: ["reasoning", "tool_calling"],
|
||||
reasoning_efforts: ["low", "medium", "high"],
|
||||
reasoning_effort_default: "medium",
|
||||
currency: "usd",
|
||||
input_per_1m: "0.150000",
|
||||
output_per_1m: "0.600000",
|
||||
},
|
||||
{
|
||||
id: "google/gemma-4-31b-it",
|
||||
name: "google/gemma-4-31b-it",
|
||||
context_window: 262144,
|
||||
capabilities: ["tool_calling", "vision", "video", "document"],
|
||||
reasoning_efforts: null,
|
||||
currency: "usd",
|
||||
input_per_1m: "0.200000",
|
||||
output_per_1m: "0.500000",
|
||||
},
|
||||
]),
|
||||
) as unknown as FetchImpl;
|
||||
|
||||
const options = aiandModelManagerOptions({ apiKey: "aiand-key", fetch: fetchMock });
|
||||
expect(options.dynamicModelsAuthoritative).toBe(true);
|
||||
const models = await options.fetchDynamicModels?.();
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"https://api.aiand.com/v1/models",
|
||||
expect.objectContaining({
|
||||
method: "GET",
|
||||
headers: expect.objectContaining({ Authorization: "Bearer aiand-key" }),
|
||||
}),
|
||||
);
|
||||
|
||||
const gptOss = models?.find(model => model.id === "openai/gpt-oss-120b");
|
||||
expect(gptOss?.name).toBe("OpenAI GPT OSS 120B");
|
||||
expect(gptOss?.reasoning).toBe(true);
|
||||
expect(gptOss?.thinking?.efforts).toEqual([Effort.Low, Effort.Medium, Effort.High]);
|
||||
expect(gptOss?.thinking?.defaultLevel).toBe(Effort.Medium);
|
||||
expect(gptOss?.contextWindow).toBe(131072);
|
||||
expect(gptOss?.cost).toEqual({ input: 0.15, output: 0.6, cacheRead: 0, cacheWrite: 0 });
|
||||
expect(gptOss?.input).toEqual(["text"]);
|
||||
|
||||
const gemma = models?.find(model => model.id === "google/gemma-4-31b-it");
|
||||
expect(gemma?.reasoning).toBe(false);
|
||||
expect(gemma?.thinking).toBeUndefined();
|
||||
expect(gemma?.input).toEqual(["text", "image"]);
|
||||
});
|
||||
|
||||
test("ignores non-USD pricing so JPY orgs do not corrupt USD cost accounting", async () => {
|
||||
const fetchMock: FetchImpl = vi.fn(async () =>
|
||||
aiandModelsResponse([
|
||||
{
|
||||
id: "zai-org/glm-5.2",
|
||||
context_window: 1000000,
|
||||
capabilities: ["reasoning", "tool_calling"],
|
||||
currency: "jpy",
|
||||
input_per_1m: "150.000000",
|
||||
output_per_1m: "600.000000",
|
||||
},
|
||||
]),
|
||||
) as unknown as FetchImpl;
|
||||
|
||||
const options = aiandModelManagerOptions({ apiKey: "aiand-key", fetch: fetchMock });
|
||||
const models = await options.fetchDynamicModels?.();
|
||||
expect(models?.[0]?.cost).toEqual({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
|
||||
});
|
||||
|
||||
test("prefers explicit base URL over AIAND_BASE_URL and appends /v1", async () => {
|
||||
Bun.env.AIAND_BASE_URL = "https://env.aiand.test";
|
||||
const fetchMock: FetchImpl = vi.fn(async () =>
|
||||
aiandModelsResponse([{ id: "openai/gpt-oss-120b" }]),
|
||||
) as unknown as FetchImpl;
|
||||
|
||||
const options = aiandModelManagerOptions({
|
||||
apiKey: "aiand-key",
|
||||
baseUrl: "https://config.aiand.test/",
|
||||
fetch: fetchMock,
|
||||
});
|
||||
await options.fetchDynamicModels?.();
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"https://config.aiand.test/v1/models",
|
||||
expect.objectContaining({ method: "GET" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -288,10 +288,13 @@ describe("fetchCursorUsableModels", () => {
|
||||
});
|
||||
|
||||
it("keeps the default window for unlabeled non-max models and max-mode models outside 1M families", async () => {
|
||||
// Unbundled ids: the contract under test is "no 1M signal → fallback
|
||||
// preserved", so neither id may carry a bundled cursor reference whose
|
||||
// snapshot window would replace the 200k default fallback.
|
||||
const response = create(GetUsableModelsResponseSchema, {
|
||||
models: [
|
||||
create(ModelDetailsSchema, { modelId: "cursor-composer-max", maxMode: true }),
|
||||
create(ModelDetailsSchema, { modelId: "claude-opus-4-8-high", displayName: "Opus 4.8" }),
|
||||
create(ModelDetailsSchema, { modelId: "claude-opus-9-high", displayName: "Opus 9" }),
|
||||
],
|
||||
});
|
||||
const defaultBaseUrl = await startCursorDiscoveryServer(toBinary(GetUsableModelsResponseSchema, response));
|
||||
@@ -299,7 +302,7 @@ describe("fetchCursorUsableModels", () => {
|
||||
const models = await fetchCursorUsableModels({ apiKey: "test-token", baseUrl: defaultBaseUrl, timeoutMs: 1_000 });
|
||||
|
||||
expect(models).toEqual([
|
||||
expect.objectContaining({ id: "claude-opus-4-8-high", cursorMaxMode: false, contextWindow: 200_000 }),
|
||||
expect.objectContaining({ id: "claude-opus-9-high", cursorMaxMode: false, contextWindow: 200_000 }),
|
||||
expect.objectContaining({ id: "cursor-composer-max", cursorMaxMode: true, contextWindow: 200_000 }),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -154,7 +154,7 @@ describe("task.batch schema gating", () => {
|
||||
expect(batch.description).toContain("`effort`");
|
||||
});
|
||||
|
||||
it("keeps isolation boolean-only and describes the configured apply behavior", async () => {
|
||||
it("keeps isolation boolean-only in the batch item schema", async () => {
|
||||
mockDiscovery();
|
||||
|
||||
const tool = await TaskTool.create(
|
||||
@@ -169,18 +169,6 @@ describe("task.batch schema gating", () => {
|
||||
}
|
||||
expect(isolatedSchema.type).toBe("boolean");
|
||||
expect(itemProperties.apply).toBeUndefined();
|
||||
expect(tool.description).toContain("automatically applied to the parent checkout");
|
||||
|
||||
const captureTool = await TaskTool.create(
|
||||
createSession({
|
||||
settings: {
|
||||
"task.batch": true,
|
||||
"task.isolation.mode": "auto",
|
||||
"task.isolation.apply": false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(captureTool.description).toContain("without modifying the parent checkout");
|
||||
});
|
||||
|
||||
it("hides isolation from the dynamic batch schema in plan mode", async () => {
|
||||
|
||||
Reference in New Issue
Block a user