← back to AbramsOS
chat: add funded OpenAI GPT-5.2 external lane (Responses API) through the redact+NER boundary (TK-11084)
fe14679e26fa89b697a2f37022eb5ffd6789a89f · 2026-09-01 17:43:15 -0700 · Steve
Files touched
M lib/chat-providers/index.jsA lib/chat-providers/openai.jsM lib/chat-registry.jsM tests/chat.test.js
Diff
commit fe14679e26fa89b697a2f37022eb5ffd6789a89f
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Sep 1 17:43:15 2026 -0700
chat: add funded OpenAI GPT-5.2 external lane (Responses API) through the redact+NER boundary (TK-11084)
---
lib/chat-providers/index.js | 3 +++
lib/chat-providers/openai.js | 62 ++++++++++++++++++++++++++++++++++++++++++++
lib/chat-registry.js | 11 ++++++++
tests/chat.test.js | 28 +++++++++++++++++++-
4 files changed, 103 insertions(+), 1 deletion(-)
diff --git a/lib/chat-providers/index.js b/lib/chat-providers/index.js
index 3921b09..f603e8f 100644
--- a/lib/chat-providers/index.js
+++ b/lib/chat-providers/index.js
@@ -3,6 +3,7 @@
const gemini = require('./gemini');
const ollama = require('./ollama');
const claudeCli = require('./claude-cli');
+const openai = require('./openai');
async function dispatch(model, { system, messages }) {
const args = { system, messages, providerModel: model.providerModel };
@@ -13,6 +14,8 @@ async function dispatch(model, { system, messages }) {
return ollama.chat(args);
case 'claude-cli':
return claudeCli.chat(args);
+ case 'openai':
+ return openai.chat(args);
default:
throw new Error(`unknown provider: ${model.provider}`);
}
diff --git a/lib/chat-providers/openai.js b/lib/chat-providers/openai.js
new file mode 100644
index 0000000..850b897
--- /dev/null
+++ b/lib/chat-providers/openai.js
@@ -0,0 +1,62 @@
+// OpenAI adapter — EXTERNAL lane. Mirrors the proven `ask-openai` call path
+// (Responses API, gpt-5.2). Prompt is already redacted + NER-scrubbed by the
+// route before it reaches here (AGENTS.md: no PII to a model provider without
+// redaction). AGENTS.md bans only the ANTHROPIC key — OpenAI is permitted.
+
+const BASE = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1';
+const CHAT_TIMEOUT_MS = 60_000;
+
+function key() {
+ const k = process.env.OPENAI_API_KEY;
+ if (!k) throw new Error('OPENAI_API_KEY not set');
+ return k;
+}
+
+// Pull the text out of a Responses-API payload (output_text convenience field,
+// else the output[].content[] blocks of type output_text).
+function extractText(j) {
+ if (typeof j.output_text === 'string' && j.output_text.trim()) return j.output_text.trim();
+ const parts = [];
+ for (const item of j.output || []) {
+ for (const c of item.content || []) {
+ if (c.type === 'output_text' && typeof c.text === 'string') parts.push(c.text);
+ }
+ }
+ return parts.join('').trim();
+}
+
+async function chat({ system, messages, providerModel }) {
+ const model = providerModel || 'gpt-5.2';
+ // Responses API accepts an array of {role, content} as `input`; `instructions`
+ // carries the system prompt.
+ const input = (messages || []).map((m) => ({
+ role: m.role === 'assistant' ? 'assistant' : 'user',
+ content: String(m.content ?? ''),
+ }));
+
+ const ctrl = new AbortController();
+ const t = setTimeout(() => ctrl.abort(), CHAT_TIMEOUT_MS);
+ try {
+ const r = await fetch(`${BASE}/responses`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${key()}`,
+ },
+ body: JSON.stringify({ model, instructions: system || undefined, input }),
+ signal: ctrl.signal,
+ });
+ if (!r.ok) {
+ const body = (await r.text().catch(() => '')).slice(0, 300);
+ throw new Error(`openai ${r.status}: ${body}`);
+ }
+ const j = await r.json();
+ const text = extractText(j);
+ if (!text) throw new Error('openai returned empty response');
+ return { text };
+ } finally {
+ clearTimeout(t);
+ }
+}
+
+module.exports = { chat, extractText, BASE };
diff --git a/lib/chat-registry.js b/lib/chat-registry.js
index fd3d204..6ee6a52 100644
--- a/lib/chat-registry.js
+++ b/lib/chat-registry.js
@@ -62,6 +62,17 @@ const MODELS = [
cost: 'Max-plan (no metered API)',
dataNote: 'External — Anthropic via local CLI. PII is redacted before send.',
},
+ {
+ id: 'openai-gpt-5.2',
+ label: 'GPT-5.2 (OpenAI)',
+ provider: 'openai',
+ providerModel: 'gpt-5.2',
+ visibility: 'external',
+ contextWindow: 'large',
+ speed: 'medium',
+ cost: 'metered · OpenAI account',
+ dataNote: 'External — OpenAI Responses API. PII is redacted + NER-scrubbed before send.',
+ },
];
// Each agent = a system prompt + a data-scope. `scope` names the grounding
diff --git a/tests/chat.test.js b/tests/chat.test.js
index ac116fb..42e0e1b 100644
--- a/tests/chat.test.js
+++ b/tests/chat.test.js
@@ -78,7 +78,7 @@ test('registry defaults resolve and every model has a known visibility', () => {
assert.ok(registry.AGENT_BY_ID[registry.DEFAULT_AGENT_ID], 'default agent missing');
for (const m of registry.MODELS) {
assert.ok(['external', 'local'].includes(m.visibility), `bad visibility on ${m.id}`);
- assert.ok(['gemini', 'ollama', 'claude-cli'].includes(m.provider), `bad provider on ${m.id}`);
+ assert.ok(['gemini', 'ollama', 'claude-cli', 'openai'].includes(m.provider), `bad provider on ${m.id}`);
}
// Ollama lanes must be local (raw PII allowed); gemini/claude must be external.
for (const m of registry.MODELS) {
@@ -289,3 +289,29 @@ test('ner.detectNames: FAIL-CLOSED — throws when local Ollama is unavailable',
);
});
});
+
+// ── OpenAI (GPT) lane: Responses-API parse + registry membership ─────────────
+const openaiAdapter = require('../lib/chat-providers/openai');
+
+test('openai.extractText: reads output_text convenience field', () => {
+ assert.strictEqual(openaiAdapter.extractText({ output_text: 'hello' }), 'hello');
+});
+
+test('openai.extractText: reads output[].content[] blocks', () => {
+ const j = { output: [{ content: [{ type: 'output_text', text: 'grounded ' }, { type: 'output_text', text: 'answer' }] }] };
+ assert.strictEqual(openaiAdapter.extractText(j), 'grounded answer');
+});
+
+test('openai.chat: parses a stubbed Responses payload', async () => {
+ await withFetch(async () => ({ ok: true, json: async () => ({ output_text: 'pong' }), text: async () => '' }), async () => {
+ const { text } = await openaiAdapter.chat({ system: 'sys', messages: [{ role: 'user', content: 'ping' }], providerModel: 'gpt-5.2' });
+ assert.strictEqual(text, 'pong');
+ });
+});
+
+test('registry: OpenAI GPT lane is registered as an EXTERNAL (redacted) model', () => {
+ const m = registry.MODEL_BY_ID['openai-gpt-5.2'];
+ assert.ok(m, 'openai-gpt-5.2 missing from registry');
+ assert.strictEqual(m.provider, 'openai');
+ assert.strictEqual(m.visibility, 'external', 'OpenAI lane must be external so PII is redacted before send');
+});
← 9098e9c chat: guard nav-bar chat behind authed userId (hide on unaut
·
back to AbramsOS
·
chat: default lane = GPT-5.2 (funded external) instead of ol a0e1b82 →