Skip to content
Open
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
4 changes: 2 additions & 2 deletions shim/codex-oauth.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import os from "node:os";
import path from "node:path";

import { frames, resolveEntry } from "./inference.mjs";
import { structToJson, valueToJson } from "./struct.mjs";
import { structToJson, structToJsonSchema, valueToJson } from "./struct.mjs";

const OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
const OAUTH_TOKEN_URL = process.env.CODEX_OAUTH_TOKEN_URL ?? "https://auth.openai.com/oauth/token";
Expand Down Expand Up @@ -255,7 +255,7 @@ function buildRequestBody(req, entry, { withReasoningHistory }) {
name: t.name,
description: t.description ?? "",
strict: false,
parameters: structToJson(t.parameters) ?? { type: "object", properties: {} },
parameters: structToJsonSchema(t.parameters) ?? { type: "object", properties: {} },
}));

const body = {
Expand Down
74 changes: 60 additions & 14 deletions shim/inference.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import protobuf from "protobufjs";

import { structToJson } from "./struct.mjs";
import { structToJson, structToJsonSchema, valueToJson } from "./struct.mjs";

const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");

Expand Down Expand Up @@ -106,30 +106,76 @@ function sseLines(chunkText, leftover) {
return { lines: lines.slice(0, -1), leftover: lines.at(-1) };
}

function messageText(message) {
if (message.content === "text") return message.text ?? "";
if (message.content === "parts") {
return (message.parts?.parts ?? [])
.map((part) => (part.part === "text" ? part.text?.text ?? "" : ""))
.join("");
}
return "";
}

function toolResultText(part) {
const value = valueToJson(part.result);
const text = typeof value === "string" ? value : JSON.stringify(value ?? null);
return part.isError ? `ERROR: ${text}` : text;
}

function openAiMessages(messages) {
const out = [];
for (const message of messages ?? []) {
if (message.role === 2) {
const content = messageText(message);
const toolCalls = (message.toolCalls ?? [])
.filter((call) => call.toolCallId && call.toolName)
.map((call) => ({
id: call.toolCallId,
type: "function",
function: {
name: call.toolName,
arguments: call.rawToolCallArgs || JSON.stringify(structToJson(call.args) ?? {}),
},
}));
if (content || toolCalls.length) {
out.push({
role: "assistant",
content: content || null,
...(toolCalls.length ? { tool_calls: toolCalls } : {}),
});
}
continue;
}
if (message.role === 3) {
for (const part of message.toolContent?.parts ?? []) {
if (!part.toolCallId) continue;
out.push({
role: "tool",
tool_call_id: part.toolCallId,
content: toolResultText(part),
});
}
continue;
}
const content = messageText(message);
if (content) out.push({ role: message.role === 1 ? "user" : "system", content });
}
return out;
}

async function* openaiSession(req, entry) {
const apiKey = entry.api_key ?? (entry.env_key ? process.env[entry.env_key] : undefined);
if (!apiKey) {
yield frames.error(`shim: no API key for model entry (set ${entry.env_key ?? "api_key"})`, 5);
return;
}
const messages = [];
for (const m of req.messages) {
const role = m.role === 1 ? "user" : m.role === 2 ? "assistant" : m.role === 3 ? "tool" : "system";
let content = "";
if (m.content === "text") content = m.text ?? "";
else if (m.content === "parts") {
content = (m.parts.parts ?? [])
.map((p) => (p.part === "text" ? p.text?.text ?? "" : ""))
.join("");
}
if (content) messages.push({ role, content });
}
const messages = openAiMessages(req.messages);
const tools = (req.tools ?? []).map((t) => ({
type: "function",
function: {
name: t.name,
description: t.description ?? "",
parameters: structToJson(t.parameters) ?? { type: "object", properties: {} },
parameters: structToJsonSchema(t.parameters) ?? { type: "object", properties: {} },
},
}));
const body = {
Expand Down
7 changes: 7 additions & 0 deletions shim/struct.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ export function structToJson(struct) {
return out;
}

export function structToJsonSchema(struct) {
const value = structToJson(struct);
const nested = value?.jsonSchema;
if (nested && typeof nested === "object" && !Array.isArray(nested)) return nested;
return value;
}

export function jsonToValue(value) {
if (value === null || value === undefined) return { nullValue: 0 };
if (typeof value === "number") return { numberValue: value };
Expand Down
23 changes: 23 additions & 0 deletions shimctl.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ mkdir -p "$ROOT/logs" "$ROOT/state"
is_up() { curl -sk --max-time 2 https://localhost:8443/health >/dev/null 2>&1; }
container_mode() { [[ "$(uname -s)" == "Darwin" && -n "${GROKBOT_RUNTIME_DIR:-}" ]]; }

provider_env_keys() {
node - "$ROOT/models.json" <<'NODE'
const fs = require("node:fs");

const config = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const seen = new Set();
for (const entry of Object.values(config.models ?? {})) {
const key = entry?.env_key;
if (typeof key !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
if (process.env[key] === undefined || seen.has(key)) continue;
seen.add(key);
process.stdout.write(`${key}\n`);
}
NODE
}

stop() {
if container_mode; then
docker exec "${GROKBOT_COMPUTER_CONTAINER:-grokbot-computer}" pkill -f '/grokbot-shim/shim/server.mjs' 2>/dev/null || true
Expand All @@ -33,12 +49,19 @@ stop() {

start() {
if container_mode; then
local provider_keys
local -a provider_args=()
provider_keys="$(provider_env_keys)"
while IFS= read -r key; do
[[ -n "$key" ]] && provider_args+=(-e "$key")
done <<<"$provider_keys"
docker exec -d \
-u "$(id -u):$(id -g)" \
-w /grokbot-shim \
-e ELECTRON_RUN_AS_NODE=1 \
-e BIND_HOST=0.0.0.0 \
-e CODEX_AUTH_FILE=/codex/auth.json \
"${provider_args[@]}" \
"${GROKBOT_COMPUTER_CONTAINER:-grokbot-computer}" \
"${GROKBOT_HOST_APP:-/opt/Grok Bot/sand}" /grokbot-shim/shim/server.mjs
for _ in $(seq 1 20); do is_up && break; sleep 0.25; done
Expand Down
104 changes: 104 additions & 0 deletions test/inference.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import assert from "node:assert/strict";
import test from "node:test";

import { runEntry } from "../shim/inference.mjs";
import { jsonToStruct, jsonToValue } from "../shim/struct.mjs";

async function captureOpenAiRequest(t, request) {
const originalFetch = globalThis.fetch;
let captured;
globalThis.fetch = async (url, options) => {
captured = { url: String(url), body: JSON.parse(options.body) };
return new Response(
'data: {"id":"test-response","model":"test-model","choices":[{"delta":{},"finish_reason":"stop"}]}\n\ndata: [DONE]\n\n',
{ status: 200, headers: { "content-type": "text/event-stream" } },
);
};
t.after(() => {
globalThis.fetch = originalFetch;
});

const entry = {
provider: "openai-compatible",
base_url: "https://provider.example/v1",
model: "test-model",
api_key: "fixture-key",
};
for await (const _frame of runEntry(request, entry)) {
// Consume the real adapter stream so the outbound request is completed.
}
return captured;
}

test("OpenAI-compatible tools unwrap Grok's nested JSON Schema", async (t) => {
const nestedSchema = {
type: "object",
properties: { pattern: { type: "string" } },
};
const captured = await captureOpenAiRequest(t, {
messages: [],
tools: [
{
name: "AwaitExternalShell",
description: "Wait for output",
parameters: jsonToStruct({ jsonSchema: nestedSchema }),
},
],
modelConfig: {},
});

assert.deepEqual(captured.body.tools[0].function.parameters, nestedSchema);
});

test("OpenAI-compatible history preserves tool calls and results", async (t) => {
const captured = await captureOpenAiRequest(t, {
messages: [
{ role: 1, content: "text", text: "Open app.radprimer.com" },
{
role: 2,
toolCalls: [
{
toolCallId: "call-browser-1",
toolName: "OpenBrowser",
rawToolCallArgs: '{"url":"https://app.radprimer.com"}',
},
],
},
{
role: 3,
content: "toolContent",
toolContent: {
parts: [
{
toolCallId: "call-browser-1",
toolName: "OpenBrowser",
result: jsonToValue({ opened: true }),
isError: false,
},
],
},
},
],
tools: [],
modelConfig: {},
});

assert.deepEqual(captured.body.messages, [
{ role: "user", content: "Open app.radprimer.com" },
{
role: "assistant",
content: null,
tool_calls: [
{
id: "call-browser-1",
type: "function",
function: {
name: "OpenBrowser",
arguments: '{"url":"https://app.radprimer.com"}',
},
},
],
},
{ role: "tool", tool_call_id: "call-browser-1", content: '{"opened":true}' },
]);
});
11 changes: 6 additions & 5 deletions test/models.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,23 @@ import {
} from "../shim/models.mjs";
import { statsigBootstrap } from "../shim/statsig.mjs";

const modelConfig = JSON.parse(fs.readFileSync(new URL("../models.json", import.meta.url), "utf8"));

test("the model catalog exposes the configured default", () => {
const payload = availableModelsPayload();
assert.equal(payload.composerModelConfig.defaultModel, "GPT-5.6-Luna (Codex)");
assert.ok(payload.models.some((model) => model.name === "GPT-5.6-Luna (Codex)"));
assert.equal(payload.composerModelConfig.defaultModel, modelConfig.default);
assert.ok(payload.models.some((model) => model.name === modelConfig.default && model.defaultOn));
assert.ok(payload.models.every((model) => model.visibleInRoutedModelView === false));
});

test("the model catalog survives protobuf encoding", () => {
const decoded = decodeAvailableModels(encodeAvailableModels());
assert.equal(decoded.models.length, availableModelsPayload().models.length);
assert.equal(decoded.composerModelConfig.defaultModel, "GPT-5.6-Luna (Codex)");
assert.equal(decoded.composerModelConfig.defaultModel, modelConfig.default);
});

test("tracked model configuration contains no inline API keys", () => {
const config = JSON.parse(fs.readFileSync(new URL("../models.json", import.meta.url), "utf8"));
for (const entry of Object.values(config.models)) {
for (const entry of Object.values(modelConfig.models)) {
assert.equal("api_key" in entry, false);
}
});
Expand Down
65 changes: 65 additions & 0 deletions test/shimctl.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";

const ROOT = path.resolve(new URL("..", import.meta.url).pathname);

function writeExecutable(file, contents) {
fs.writeFileSync(file, contents, { mode: 0o755 });
}

test("container shim start forwards only configured provider environment names", (t) => {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), "grokbot-shimctl-test-"));
t.after(() => fs.rmSync(fixture, { recursive: true, force: true }));

fs.mkdirSync(path.join(fixture, "scripts"));
fs.copyFileSync(path.join(ROOT, "shimctl.sh"), path.join(fixture, "shimctl.sh"));
fs.copyFileSync(path.join(ROOT, "scripts", "load-env.sh"), path.join(fixture, "scripts", "load-env.sh"));
fs.writeFileSync(
path.join(fixture, ".env"),
[
"GROKBOT_RUNTIME_DIR=/tmp/grokbot-runtime",
'GROKBOT_HOST_APP="/opt/Grok Bot/grok-bot"',
"CUSTOM_PROVIDER_TOKEN=fixture-secret",
"",
].join("\n"),
);
fs.writeFileSync(
path.join(fixture, "models.json"),
JSON.stringify({
models: {
first: { provider: "openai-compatible", env_key: "CUSTOM_PROVIDER_TOKEN" },
duplicate: { provider: "openai-compatible", env_key: "CUSTOM_PROVIDER_TOKEN" },
missing: { provider: "openai-compatible", env_key: "MISSING_PROVIDER_TOKEN" },
invalid: { provider: "openai-compatible", env_key: "INVALID-NAME" },
},
}),
);

const fakeBin = path.join(fixture, "bin");
fs.mkdirSync(fakeBin);
const dockerArgsFile = path.join(fixture, "docker-args.txt");
writeExecutable(path.join(fakeBin, "uname"), "#!/bin/sh\necho Darwin\n");
writeExecutable(path.join(fakeBin, "curl"), "#!/bin/sh\nexit 0\n");
writeExecutable(
path.join(fakeBin, "docker"),
'#!/bin/sh\nprintf "%s\\n" "$@" > "$DOCKER_ARGS_FILE"\n',
);

execFileSync("bash", [path.join(fixture, "shimctl.sh"), "start"], {
env: {
...process.env,
DOCKER_ARGS_FILE: dockerArgsFile,
PATH: `${fakeBin}:${process.env.PATH}`,
},
});

const args = fs.readFileSync(dockerArgsFile, "utf8").trim().split("\n");
assert.equal(args.filter((arg) => arg === "CUSTOM_PROVIDER_TOKEN").length, 1);
assert.equal(args.includes("MISSING_PROVIDER_TOKEN"), false);
assert.equal(args.includes("INVALID-NAME"), false);
assert.equal(args.some((arg) => arg.includes("fixture-secret")), false);
});