fix(compaction): preserve provider defaults for remote compaction
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
*/
|
||||
|
||||
import { ProviderHttpError } from "@oh-my-pi/pi-ai/errors";
|
||||
import { parseTextSignature } from "@oh-my-pi/pi-ai/providers/openai-shared";
|
||||
import { parseAzureDeploymentNameMap, parseTextSignature } from "@oh-my-pi/pi-ai/providers/openai-shared";
|
||||
import { transformMessages } from "@oh-my-pi/pi-ai/providers/transform-messages";
|
||||
import type { Api, AssistantMessage, FetchImpl, Message, Model } from "@oh-my-pi/pi-ai/types";
|
||||
import {
|
||||
@@ -144,6 +144,14 @@ function appendAzureApiVersion(endpoint: string): string {
|
||||
return `${endpoint}${separator}api-version=${encodeURIComponent($env.AZURE_OPENAI_API_VERSION || DEFAULT_AZURE_API_VERSION)}`;
|
||||
}
|
||||
|
||||
function resolveOpenAiCompactModel(model: Model): string {
|
||||
const requestModel = model.remoteCompaction?.model ?? model.requestModelId ?? model.id;
|
||||
const compactionApi = model.remoteCompaction?.api ?? model.api;
|
||||
if (compactionApi !== "azure-openai-responses") return requestModel;
|
||||
const mappedDeployment = parseAzureDeploymentNameMap($env.AZURE_OPENAI_DEPLOYMENT_NAME_MAP).get(requestModel);
|
||||
return mappedDeployment ?? requestModel;
|
||||
}
|
||||
|
||||
function resolveOpenAiCodexCompactEndpoint(baseUrl: string | undefined): string {
|
||||
const rawBase = baseUrl && baseUrl.length > 0 ? baseUrl : CODEX_BASE_URL;
|
||||
const normalizedBase = rawBase.endsWith("/") ? rawBase.slice(0, -1) : rawBase;
|
||||
@@ -495,7 +503,7 @@ export async function requestOpenAiRemoteCompaction(
|
||||
opts?: { fetch?: FetchImpl; timeoutMs?: number },
|
||||
): Promise<OpenAiRemoteCompactionResponse> {
|
||||
const endpoint = resolveOpenAiCompactEndpoint(model);
|
||||
const requestModel = model.remoteCompaction?.model ?? model.requestModelId ?? model.id;
|
||||
const requestModel = resolveOpenAiCompactModel(model);
|
||||
const request: OpenAiRemoteCompactionRequest = {
|
||||
model: requestModel,
|
||||
input: trimOpenAiCompactInput(compactInput, model.contextWindow ?? Number.POSITIVE_INFINITY, instructions),
|
||||
|
||||
@@ -286,6 +286,8 @@ test("uses configured OpenAI-compatible compaction for custom providers", async
|
||||
});
|
||||
|
||||
test("uses Azure request shape for Azure Responses remote compaction", async () => {
|
||||
const previousDeploymentMap = Bun.env.AZURE_OPENAI_DEPLOYMENT_NAME_MAP;
|
||||
Bun.env.AZURE_OPENAI_DEPLOYMENT_NAME_MAP = "gpt-5-compact=azure-gpt-5-compact";
|
||||
const model = makeAzureModel({
|
||||
headers: { "x-custom-header": "custom" },
|
||||
remoteCompaction: {
|
||||
@@ -332,7 +334,12 @@ test("uses Azure request shape for Azure Responses remote compaction", async ()
|
||||
expect(requestAuthorization).toBeUndefined();
|
||||
expect(requestContentType).toBe("application/json");
|
||||
expect(requestCustomHeader).toBe("custom");
|
||||
expect(requestBody).toMatchObject({ model: "gpt-5-compact" });
|
||||
expect(requestBody).toMatchObject({ model: "azure-gpt-5-compact" });
|
||||
if (previousDeploymentMap === undefined) {
|
||||
delete Bun.env.AZURE_OPENAI_DEPLOYMENT_NAME_MAP;
|
||||
} else {
|
||||
Bun.env.AZURE_OPENAI_DEPLOYMENT_NAME_MAP = previousDeploymentMap;
|
||||
}
|
||||
});
|
||||
|
||||
describe("requestOpenAiRemoteCompaction abort", () => {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* discovery lives in pi-catalog's provider-models.
|
||||
*/
|
||||
import { type ApiKey, type FetchImpl, withAuth } from "@oh-my-pi/pi-ai";
|
||||
import type { Api, Model } from "@oh-my-pi/pi-ai/types";
|
||||
import type { Api, Model, RemoteCompactionConfig } from "@oh-my-pi/pi-ai/types";
|
||||
import { buildModel } from "@oh-my-pi/pi-catalog/build";
|
||||
import {
|
||||
getBundledModelReferenceIndex,
|
||||
@@ -95,6 +95,7 @@ export interface DiscoveryProviderConfig {
|
||||
baseUrl?: string;
|
||||
headers?: Record<string, string>;
|
||||
compat?: ModelSpec<Api>["compat"];
|
||||
remoteCompaction?: RemoteCompactionConfig<Api>;
|
||||
discovery: ProviderDiscovery;
|
||||
optional?: boolean;
|
||||
}
|
||||
|
||||
@@ -146,8 +146,8 @@ export function mergeDiscoveredModel<TApi extends Api>(
|
||||
baseUrl: providerOverride?.baseUrl ?? model.baseUrl ?? existing.baseUrl,
|
||||
headers: existing.headers ? { ...existing.headers, ...model.headers } : model.headers,
|
||||
transport: providerOverride?.transport ?? existing.transport ?? model.transport,
|
||||
remoteCompaction: mergeRemoteCompactionConfig(
|
||||
existing.remoteCompaction ?? model.remoteCompaction,
|
||||
remoteCompaction: mergeProviderRemoteCompactionConfig(
|
||||
mergeRemoteCompactionConfig(existing.remoteCompaction, model.remoteCompaction),
|
||||
providerOverride?.remoteCompaction,
|
||||
),
|
||||
...(supportsTools !== undefined ? { supportsTools } : {}),
|
||||
@@ -160,7 +160,7 @@ export function mergeDiscoveredModel<TApi extends Api>(
|
||||
baseUrl: providerOverride.baseUrl ?? model.baseUrl,
|
||||
headers: providerOverride.headers ? { ...model.headers, ...providerOverride.headers } : model.headers,
|
||||
...(providerOverride.transport !== undefined ? { transport: providerOverride.transport } : {}),
|
||||
remoteCompaction: mergeRemoteCompactionConfig(model.remoteCompaction, providerOverride.remoteCompaction),
|
||||
remoteCompaction: mergeProviderRemoteCompactionConfig(model.remoteCompaction, providerOverride.remoteCompaction),
|
||||
compat: model.compatConfig,
|
||||
} as ModelSpec<TApi>);
|
||||
}
|
||||
@@ -377,6 +377,13 @@ function mergeRemoteCompactionConfig(
|
||||
return { ...baseConfig, ...overrideConfig };
|
||||
}
|
||||
|
||||
function mergeProviderRemoteCompactionConfig(
|
||||
modelConfig: RemoteCompactionConfig<Api> | undefined,
|
||||
providerConfig: RemoteCompactionConfig<Api> | undefined,
|
||||
): RemoteCompactionConfig<Api> | undefined {
|
||||
return mergeRemoteCompactionConfig(providerConfig, modelConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Project a built model back to spec shape for the model-manager/cache
|
||||
* boundary: sparse compat comes from `compatConfig`, never from the resolved
|
||||
@@ -1111,12 +1118,25 @@ export class ModelRegistry {
|
||||
)
|
||||
: models;
|
||||
|
||||
const withRemoteCompaction = providerConfig.remoteCompaction
|
||||
? withDecoderMetadata.map(model =>
|
||||
buildModel({
|
||||
...model,
|
||||
remoteCompaction: mergeProviderRemoteCompactionConfig(
|
||||
model.remoteCompaction,
|
||||
providerConfig.remoteCompaction,
|
||||
),
|
||||
compat: model.compatConfig,
|
||||
} as ModelSpec<Api>),
|
||||
)
|
||||
: withDecoderMetadata;
|
||||
|
||||
if (providerConfig.provider !== "ollama" || providerConfig.api !== "openai-responses") {
|
||||
return withDecoderMetadata;
|
||||
return withRemoteCompaction;
|
||||
}
|
||||
|
||||
const contextLengthOverride = getOllamaContextLengthOverride();
|
||||
return withDecoderMetadata.map(model => {
|
||||
return withRemoteCompaction.map(model => {
|
||||
const normalized =
|
||||
model.api === "openai-completions"
|
||||
? buildModel({
|
||||
@@ -1249,6 +1269,7 @@ export class ModelRegistry {
|
||||
baseUrl: providerConfig.baseUrl,
|
||||
headers: resolvedProviderHeaders,
|
||||
compat: mergeCompat(providerConfig.compat, disableStrictCompat),
|
||||
remoteCompaction: providerConfig.remoteCompaction,
|
||||
discovery: providerConfig.discovery,
|
||||
optional: false,
|
||||
});
|
||||
@@ -1664,7 +1685,7 @@ export class ModelRegistry {
|
||||
// Preserve the model's existing transport when the override omits one;
|
||||
// providers without a `transport` field keep the default per-API dispatch.
|
||||
...(override.transport !== undefined ? { transport: override.transport } : {}),
|
||||
remoteCompaction: mergeRemoteCompactionConfig(entry.remoteCompaction, override.remoteCompaction),
|
||||
remoteCompaction: mergeProviderRemoteCompactionConfig(entry.remoteCompaction, override.remoteCompaction),
|
||||
};
|
||||
}
|
||||
#applyRuntimeProviderOverrides(models: Model<Api>[]): Model<Api>[] {
|
||||
|
||||
@@ -181,6 +181,47 @@ describe("ModelRegistry runtime provider registration", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("refreshRuntimeProviders preserves model-level remoteCompaction over provider defaults", async () => {
|
||||
const providerName = "dynamic-compact-provider";
|
||||
const providerEndpoint = "https://runtime.example.com/v1/responses/provider-compact";
|
||||
const modelEndpoint = "https://runtime.example.com/v1/responses/model-compact";
|
||||
|
||||
registry.registerProvider(
|
||||
providerName,
|
||||
{
|
||||
baseUrl: "https://runtime.example.com/v1",
|
||||
apiKey: "RUNTIME_KEY",
|
||||
api: "openai-responses",
|
||||
remoteCompaction: {
|
||||
enabled: true,
|
||||
api: "openai-responses",
|
||||
endpoint: providerEndpoint,
|
||||
model: "provider-compact",
|
||||
},
|
||||
fetchDynamicModels: async () => [
|
||||
{
|
||||
...baseModel,
|
||||
id: "dynamic-compact-model",
|
||||
remoteCompaction: {
|
||||
endpoint: modelEndpoint,
|
||||
model: "model-compact",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"ext://runtime",
|
||||
);
|
||||
|
||||
await registry.refreshRuntimeProviders("online");
|
||||
const model = registry.find(providerName, "dynamic-compact-model");
|
||||
expect(model?.remoteCompaction).toEqual({
|
||||
enabled: true,
|
||||
api: "openai-responses",
|
||||
endpoint: modelEndpoint,
|
||||
model: "model-compact",
|
||||
});
|
||||
});
|
||||
|
||||
test("registerProvider preserves explicit thinking and backfills wire facts", () => {
|
||||
const config: ProviderConfigInput = {
|
||||
baseUrl: "https://runtime.example.com/v1",
|
||||
|
||||
@@ -1917,6 +1917,7 @@ describe("ModelRegistry", () => {
|
||||
let specialCache: ModelRegistry;
|
||||
let vertexAuthoritative: ModelRegistry;
|
||||
let syntheticCacheLoad: ModelRegistry;
|
||||
let cachedDiscoverableRemoteCompaction: ModelRegistry;
|
||||
let vertexNonAuthoritative: ModelRegistry;
|
||||
let vertexStale: ModelRegistry;
|
||||
const vertexProjectModel = () =>
|
||||
@@ -2123,6 +2124,49 @@ describe("ModelRegistry", () => {
|
||||
),
|
||||
},
|
||||
);
|
||||
cachedDiscoverableRemoteCompaction = readonlyRegistry(
|
||||
{
|
||||
providers: {
|
||||
"cached-compact-proxy": {
|
||||
baseUrl: "https://compact-proxy.example.com/v1",
|
||||
apiKey: "TEST_KEY",
|
||||
api: "openai-responses",
|
||||
discovery: { type: "openai-models-list" },
|
||||
remoteCompaction: {
|
||||
enabled: true,
|
||||
api: "openai-responses",
|
||||
endpoint: "https://compact-proxy.example.com/v1/responses/provider-compact",
|
||||
model: "provider-compact",
|
||||
},
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
seedCache: dbPath =>
|
||||
writeModelCache(
|
||||
"cached-compact-proxy:openai-models-list-context-v2",
|
||||
Date.now(),
|
||||
[
|
||||
buildModel({
|
||||
id: "cached-compact-model",
|
||||
name: "Cached Compact Model",
|
||||
api: "openai-responses",
|
||||
provider: "cached-compact-proxy",
|
||||
baseUrl: "https://compact-proxy.example.com/v1",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128_000,
|
||||
maxTokens: 16_384,
|
||||
}),
|
||||
],
|
||||
true,
|
||||
"",
|
||||
dbPath,
|
||||
),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("legacy cached discovery sentinels are ignored after nullable limit cutover", () => {
|
||||
@@ -2151,6 +2195,15 @@ describe("ModelRegistry", () => {
|
||||
expect(specialCache.find("openai-codex", "gpt-5.4-codex-pro")?.maxTokens).toBe(128_000);
|
||||
});
|
||||
|
||||
test("applies provider remoteCompaction to cached configured discovery models", () => {
|
||||
expect(cachedDiscoverableRemoteCompaction.find("cached-compact-proxy", "cached-compact-model")?.remoteCompaction).toEqual({
|
||||
enabled: true,
|
||||
api: "openai-responses",
|
||||
endpoint: "https://compact-proxy.example.com/v1/responses/provider-compact",
|
||||
model: "provider-compact",
|
||||
});
|
||||
});
|
||||
|
||||
test("replaces bundled google-vertex models with authoritative Vertex project discovery", () => {
|
||||
const vertexModels = getModelsForProvider(vertexAuthoritative, "google-vertex");
|
||||
expect(vertexModels.map(model => model.id)).toEqual(["zai-org/glm-4.7-maas"]);
|
||||
|
||||
Reference in New Issue
Block a user