Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions src/lib/inference/onboard-probes-curl-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,7 @@ export function makeFakeCurlScript(bodyLogic: string): string {
return `${FAKE_CURL_HEADER}${bodyLogic}`;
}

// Fake curl for the strict Responses API compatibility check. It records each
// requested URL, returns a successful Responses payload without a tool call,
// then returns a successful Chat Completions payload so callers can assert the
// exact fallback order without duplicating shell parsing in a test body.
export function makeResponsesFallbackUrlRecordingFakeCurlScript(): string {
function makeUrlRecordingFakeCurlScript(responsesPayload: string): string {
return `#!/usr/bin/env bash
outfile=""
url=""
Expand All @@ -58,14 +54,33 @@ n=$((n + 1))
echo "$n" > "${HARNESS_COUNTER}"
printf '%s' "$url" > "${HARNESS_TMPDIR}/request-$n-url.txt"
if echo "$url" | grep -q '/responses$'; then
printf '%s' '{"output":[{"type":"message","content":[{"type":"output_text","text":"OK"}]}]}' > "$outfile"
printf '%s' '${responsesPayload}' > "$outfile"
else
printf '%s' '{"choices":[{"message":{"content":"OK"}}]}' > "$outfile"
fi
printf '200'
`;
}

// Fake curl for the strict Responses API compatibility check. It records each
// requested URL, returns a successful Responses payload without a tool call,
// then returns a successful Chat Completions payload so callers can assert the
// exact fallback order without duplicating shell parsing in a test body.
export function makeResponsesFallbackUrlRecordingFakeCurlScript(): string {
return makeUrlRecordingFakeCurlScript(
'{"output":[{"type":"message","content":[{"type":"output_text","text":"OK"}]}]}',
);
}

// Fake curl for cache tests where /responses is a fully valid tool-call
// success. It lets tests prove a later chat-completions-only smoke is not
// satisfied by a Responses-only cache entry.
export function makeResponsesToolCallUrlRecordingFakeCurlScript(): string {
return makeUrlRecordingFakeCurlScript(
'{"output":[{"type":"function_call","name":"emit_ok","arguments":"{\\"value\\":\\"OK\\"}"}]}',
);
}

// Restore an env var to its pre-test value without branching at the call
// site (kept identical to the helper the test file uses so restore semantics
// are unchanged).
Expand Down
113 changes: 112 additions & 1 deletion src/lib/inference/onboard-probes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { afterEach, describe, expect, it } from "vitest";

import { captureAuthConfigPath } from "../adapters/http/auth-config-test-helpers";
import {
HARNESS_COUNTER,
HARNESS_TMPDIR,
makeFakeCurlScript,
makeResponsesToolCallUrlRecordingFakeCurlScript,
withFakeCurlProbe,
} from "./onboard-probes-curl-harness";

const {
clearOpenAiLikeProbeValidationCacheForTests,
getChatCompletionsProbeCurlArgs,
getChatCompletionsProbePayload,
getDeepSeekV4ProValidationProbeCurlArgs,
Expand All @@ -30,6 +32,10 @@ const {
const FAKE_CONFIG_PATH = "/tmp/nemoclaw-test-credential.conf";
const FAKE_CREDENTIAL_ARGS = ["--config", FAKE_CONFIG_PATH] as const;

afterEach(() => {
clearOpenAiLikeProbeValidationCacheForTests();
});

describe("OpenAI-compatible inference probe response parsing", () => {
it("detects tool-calling responses payloads conservatively", () => {
expect(
Expand Down Expand Up @@ -783,6 +789,111 @@ exit 28
);
});

it("reuses a successful chat-completions validation for a repeated chat-only probe", () => {
const body = `n=$(cat "${HARNESS_COUNTER}")
n=$((n + 1))
echo "$n" > "${HARNESS_COUNTER}"
cat <<'JSON' > "$outfile"
{"choices":[{"message":{"content":"OK"}}]}
JSON
printf '200'
exit 0
`;
withFakeCurlProbe(
{
script: makeFakeCurlScript(body),
dirPrefix: "nemoclaw-probe-cache-hit-",
},
({ counter }) => {
const first = probeOpenAiLikeEndpoint(
"https://integrate.api.nvidia.com/v1",
"nvidia/nemotron-3-super-120b-a12b",
"nvapi-cache-test",
{ skipResponsesProbe: true },
);
const second = probeOpenAiLikeEndpoint(
"https://integrate.api.nvidia.com/v1/",
"nvidia/nemotron-3-super-120b-a12b",
"nvapi-cache-test",
{ skipResponsesProbe: true },
);

expect(first).toMatchObject({ ok: true, api: "openai-completions" });
expect(second).toMatchObject({ ok: true, api: "openai-completions" });
expect(fs.readFileSync(counter, "utf8").trim()).toBe("1");
},
);
});

it("does not reuse a validation across different pinned address sets (#3771)", () => {
const body = `n=$(cat "${HARNESS_COUNTER}")
n=$((n + 1))
echo "$n" > "${HARNESS_COUNTER}"
cat <<'JSON' > "$outfile"
{"choices":[{"message":{"content":"OK"}}]}
JSON
printf '200'
exit 0
`;
withFakeCurlProbe(
{
script: makeFakeCurlScript(body),
dirPrefix: "nemoclaw-probe-cache-pins-",
},
({ counter }) => {
const first = probeOpenAiLikeEndpoint(
"https://proxy.example.com/v1",
"custom-model",
"proxy-cache-key",
{ skipResponsesProbe: true, pinnedAddresses: ["93.184.216.34"] },
);
const rebound = probeOpenAiLikeEndpoint(
"https://proxy.example.com/v1",
"custom-model",
"proxy-cache-key",
{ skipResponsesProbe: true, pinnedAddresses: ["93.184.216.35"] },
);

expect(first).toMatchObject({ ok: true, api: "openai-completions" });
expect(rebound).toMatchObject({ ok: true, api: "openai-completions" });
expect(fs.readFileSync(counter, "utf8").trim()).toBe("2");
},
);
});

it("keeps Responses-only and chat-only validations in separate cache entries", () => {
withFakeCurlProbe(
{
script: makeResponsesToolCallUrlRecordingFakeCurlScript(),
dirPrefix: "nemoclaw-probe-cache-api-",
},
({ counter, tmpDir }) => {
const responses = probeOpenAiLikeEndpoint(
"https://proxy.example.com/v1",
"custom-model",
"proxy-cache-key",
{ requireResponsesToolCalling: true },
);
const chatOnly = probeOpenAiLikeEndpoint(
"https://proxy.example.com/v1",
"custom-model",
"proxy-cache-key",
{ skipResponsesProbe: true },
);

expect(responses).toMatchObject({ ok: true, api: "openai-responses" });
expect(chatOnly).toMatchObject({ ok: true, api: "openai-completions" });
expect(fs.readFileSync(counter, "utf8").trim()).toBe("2");
expect(fs.readFileSync(path.join(tmpDir, "request-1-url.txt"), "utf8")).toBe(
"https://proxy.example.com/v1/responses",
);
expect(fs.readFileSync(path.join(tmpDir, "request-2-url.txt"), "utf8")).toBe(
"https://proxy.example.com/v1/chat/completions",
);
},
);
});

// PR #5975 review note PRA-14 (Nemotron). Pins the silent fallback so a
// future SGLang fix that removes the workaround stays observable.
it("falls back to chat-completions when /responses streaming lacks required events", () => {
Expand Down
100 changes: 96 additions & 4 deletions src/lib/inference/onboard-probes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ const {
const { isWsl } = require("../platform");
const httpProbe = require("../adapters/http/probe");
const authConfigModule = require("../adapters/http/auth-config");
const { hashCredential } = require("../security/credential-hash");
const { addTraceEvent } = require("../trace");
const {
getHostDockerInternalProbeFailure,
isHijackedDockerInternalUrl,
Expand Down Expand Up @@ -92,6 +94,8 @@ const EXTENDED_NVIDIA_ENDPOINT_VALIDATION_MODELS = new Set([
"qwen/qwen3.5-397b-a17b",
"deepseek-ai/deepseek-v4-flash",
]);
const OPENAI_LIKE_PROBE_CACHE_TTL_MS = 10 * 60 * 1000;
const openAiLikeProbeValidationCache = new Map();

// Hostnames that are normally meant for the sandbox/container host boundary.
// host.openshell.internal only resolves inside the OpenShell sandbox network,
Expand Down Expand Up @@ -232,10 +236,86 @@ function getProbeAuthMode(_provider) {
return undefined;
}

function normalizeOpenAiLikeProbeEndpoint(endpointUrl) {
return String(endpointUrl).replace(/\/+$/, "");
}

function normalizeProbeAuthMode(options = {}) {
return options.authMode === "query-param" ? "query-param" : "bearer";
}

function getOpenAiLikeProbeCacheKey(endpointUrl, model, apiKey, options = {}) {
const credentialHash = hashCredential(apiKey);
if (!credentialHash) return null;
return JSON.stringify({
endpointUrl: normalizeOpenAiLikeProbeEndpoint(endpointUrl),
model: String(model),
credentialHash,
authMode: normalizeProbeAuthMode(options),
allowHostDockerInternal: options.allowHostDockerInternal === true,
pinnedAddresses: Array.isArray(options.pinnedAddresses) ? [...options.pinnedAddresses] : null,
requirements: getOpenAiLikeProbeRequirements(options),
});
}

function getOpenAiLikeProbeRequirements(options = {}) {
return {
skipResponsesProbe: options.skipResponsesProbe === true,
requireResponsesToolCalling: options.requireResponsesToolCalling === true,
requireChatCompletionsToolCalling: options.requireChatCompletionsToolCalling === true,
probeStreaming: options.probeStreaming === true,
};
}

function isOpenAiLikeProbeCacheEntryFresh(entry) {
return entry.expiresAt > Date.now();
}

function getCachedOpenAiLikeProbeResult(cacheKey) {
if (!cacheKey) return null;
const entry = openAiLikeProbeValidationCache.get(cacheKey);
if (!entry) return null;
if (!isOpenAiLikeProbeCacheEntryFresh(entry)) {
openAiLikeProbeValidationCache.delete(cacheKey);
return null;
}
addTraceEvent("openai_like_probe_cache_hit", {
api: entry.api,
skip_responses_probe: entry.skipResponsesProbe,
require_responses_tool_calling: entry.requireResponsesToolCalling,
require_chat_completions_tool_calling: entry.requireChatCompletionsToolCalling,
probe_streaming: entry.probeStreaming,
});
return { ...entry.result };
}

function rememberOpenAiLikeProbeSuccess(cacheKey, options = {}, result = {}) {
if (!cacheKey || !result.ok || result.validated === false || !result.api) return result;
const requirements = getOpenAiLikeProbeRequirements(options);
openAiLikeProbeValidationCache.set(cacheKey, {
...requirements,
api: result.api,
result: { ...result },
expiresAt: Date.now() + OPENAI_LIKE_PROBE_CACHE_TTL_MS,
});
addTraceEvent("openai_like_probe_cache_store", {
api: result.api,
skip_responses_probe: requirements.skipResponsesProbe,
require_responses_tool_calling: requirements.requireResponsesToolCalling,
require_chat_completions_tool_calling: requirements.requireChatCompletionsToolCalling,
probe_streaming: requirements.probeStreaming,
});
return result;
}

function clearOpenAiLikeProbeValidationCacheForTests() {
openAiLikeProbeValidationCache.clear();
}

// ── Responses API probe ──────────────────────────────────────────

function probeResponsesToolCalling(endpointUrl, model, apiKey, options = {}) {
const baseUrl = String(endpointUrl).replace(/\/+$/, "");
const baseUrl = normalizeOpenAiLikeProbeEndpoint(endpointUrl);
let authConfig;
try {
authConfig = buildOpenAiLikeAuthConfig(apiKey, options);
Expand Down Expand Up @@ -298,7 +378,7 @@ function probeResponsesToolCalling(endpointUrl, model, apiKey, options = {}) {
}

function probeChatCompletionsToolCalling(endpointUrl, model, apiKey, options = {}) {
const baseUrl = String(endpointUrl).replace(/\/+$/, "");
const baseUrl = normalizeOpenAiLikeProbeEndpoint(endpointUrl);
let authConfig;
try {
authConfig = buildOpenAiLikeAuthConfig(apiKey, options);
Expand Down Expand Up @@ -655,6 +735,9 @@ function probeOpenAiLikeEndpoint(endpointUrl, model, apiKey, options = {}) {
// captured, so a second DNS lookup here cannot rebind the hostname to a
// private/internal address after the public preflight (TOCTOU — cv, #6293).
const pinnedAddresses = options.pinnedAddresses;
const cacheKey = getOpenAiLikeProbeCacheKey(endpointUrl, model, apiKey, options);
const cachedProbe = getCachedOpenAiLikeProbeResult(cacheKey);
if (cachedProbe) return cachedProbe;
let authConfig;
try {
authConfig = buildOpenAiLikeAuthConfig(apiKey, options);
Expand Down Expand Up @@ -786,7 +869,11 @@ function probeOpenAiLikeEndpoint(endpointUrl, model, apiKey, options = {}) {
};
}
}
return { ok: true, api: probe.api, label: probe.name };
return rememberOpenAiLikeProbeSuccess(cacheKey, options, {
ok: true,
api: probe.api,
label: probe.name,
});
}
if (
probe.api === "openai-completions" &&
Expand Down Expand Up @@ -837,7 +924,11 @@ function probeOpenAiLikeEndpoint(endpointUrl, model, apiKey, options = {}) {
authConfig,
});
if (retryResult.ok) {
return { ok: true, api: "openai-completions", label: "Chat Completions API" };
return rememberOpenAiLikeProbeSuccess(cacheKey, options, {
ok: true,
api: "openai-completions",
label: "Chat Completions API",
});
}
if (options.requireChatCompletionsToolCalling === true) {
failures.push({
Expand Down Expand Up @@ -902,6 +993,7 @@ module.exports = {
getKimiK26ValidationProbeCurlArgs,
getChatCompletionsProbePayload,
getChatCompletionsProbeCurlArgs,
clearOpenAiLikeProbeValidationCacheForTests,
probeResponsesToolCalling,
probeChatCompletionsToolCalling,
probeOpenAiLikeEndpoint,
Expand Down