supplier+deploy/mainnet: optional OPENAI_API_KEY for DeepSeek (and other hosted OpenAI-compatible APIs)
Adds an optional OPENAI_API_KEY env var that supplier/src/openai.ts forwards as `Authorization: Bearer <key>`. Empty/unset preserves the ChatMock behaviour (no auth header). Unlocks pointing the openai backend at DeepSeek directly (https://api.deepseek.com) — no local proxy needed. New compose: deploy/mainnet/docker-compose.supplier-deepseek.yml (third mainnet supplier, model deepseek-v4-pro, mp-suppliers-deepseek.vector.apexfusion.org). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
81
deploy/mainnet/docker-compose.supplier-deepseek.yml
Normal file
81
deploy/mainnet/docker-compose.supplier-deepseek.yml
Normal file
@@ -0,0 +1,81 @@
|
||||
# deploy/mainnet/docker-compose.supplier-deepseek.yml — Supplier — MAINNET (DeepSeek backend)
|
||||
#
|
||||
# Third mainnet supplier. LLM_BACKEND=openai pointed directly at DeepSeek's
|
||||
# hosted API (OpenAI-compatible at https://api.deepseek.com). No local proxy
|
||||
# container — DeepSeek is reachable over public HTTPS.
|
||||
#
|
||||
# Coexists with:
|
||||
# - marketplace-mainnet-supplier (ollama / qwen3.6:35b, mp-suppliers.vector.apexfusion.org)
|
||||
# - marketplace-mainnet-supplier-gpt (openai / ChatMock, mp-suppliers-gpt.vector.apexfusion.org)
|
||||
#
|
||||
# Pre-flight:
|
||||
# 1. DNS A-record: mp-suppliers-deepseek.vector.apexfusion.org → host IP
|
||||
# 2. supplier/.env.deepseek written on host (chmod 600) with at minimum:
|
||||
# SUPPLIER_PRIV_KEY_HEX, OPENAI_API_KEY, OGMIOS_URL, NETWORK_ID=1,
|
||||
# VECTOR_ZERO_TIME_MS, ESCROW_REF_UTXO, ADVERT_REF_UTXO, ADVERT_REF,
|
||||
# LIVE_CHAIN=1
|
||||
# 3. Networks already exist (created by the canonical supplier compose):
|
||||
# docker network create marketplace-mainnet-net # one-shot
|
||||
# # traefik-net is created by the Traefik project
|
||||
|
||||
name: marketplace-mainnet-supplier-deepseek
|
||||
|
||||
services:
|
||||
supplier:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: supplier/Dockerfile
|
||||
container_name: marketplace-mainnet-supplier-deepseek
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
# Self-contained env for the DeepSeek variant — separate from
|
||||
# ../../supplier/.env (ollama-backed) and ../../supplier/.env.gpt
|
||||
# (ChatMock-backed). Holds this supplier's distinct SUPPLIER_PRIV_KEY_HEX,
|
||||
# ADVERT_REF, ADVERT_*, OPENAI_API_KEY, and the shared mainnet plumbing
|
||||
# duplicated in full (NETWORK_ID, OGMIOS_URL, VECTOR_ZERO_TIME_MS,
|
||||
# ref-script UTxOs, LIVE_CHAIN). No env_file ordering / override magic.
|
||||
- path: ../../supplier/.env.deepseek
|
||||
required: false
|
||||
environment:
|
||||
LLM_BACKEND: openai
|
||||
# No /v1 suffix — supplier/src/openai.ts appends /v1/chat/completions.
|
||||
OPENAI_BASE_URL: https://api.deepseek.com
|
||||
# DeepSeek reasoning generations can be slow; mirror the GPT compose.
|
||||
OPENAI_TIMEOUT_MS: "300000"
|
||||
PORT: "8080"
|
||||
# Enable LiveOgmiosProvider so submitTx/awaitTx actually broadcast.
|
||||
# Without this the supplier silently falls back to ReadOnlyOgmiosProvider
|
||||
# and Claim/Submit never reach chain.
|
||||
LIVE_CHAIN: "1"
|
||||
# NETWORK_ID and OGMIOS_URL intentionally not pinned here — they MUST
|
||||
# come from the operator-provided supplier/.env.deepseek on a mainnet host.
|
||||
# Setting them via compose would be a footgun: a stray re-up on
|
||||
# testnet hardware could silently re-tag the supplier as mainnet.
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz >/dev/null || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
networks:
|
||||
- marketplace-mainnet-net
|
||||
- traefik-net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik-net"
|
||||
- "traefik.http.routers.mp-suppliers-deepseek-mainnet.entrypoints=web"
|
||||
- "traefik.http.routers.mp-suppliers-deepseek-mainnet.rule=Host(`mp-suppliers-deepseek.vector.apexfusion.org`)"
|
||||
- "traefik.http.services.mp-suppliers-deepseek-mainnet.loadbalancer.server.port=8080"
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
marketplace-mainnet-net:
|
||||
external: true
|
||||
name: marketplace-mainnet-net
|
||||
traefik-net:
|
||||
external: true
|
||||
name: traefik-net
|
||||
@@ -42,6 +42,20 @@ PORT=8080
|
||||
# generous. Increase if you see ollama_timeout reasons in logs.
|
||||
OLLAMA_TIMEOUT_MS=120000
|
||||
|
||||
# ─── Optional — OpenAI-compatible backend (LLM_BACKEND=openai) ────────────
|
||||
# Switch the chat job runner to POST /v1/chat/completions at OPENAI_BASE_URL
|
||||
# instead of Ollama. Used by the mainnet ChatMock-backed supplier and any
|
||||
# hosted OpenAI-compatible API (OpenAI, DeepSeek, etc.).
|
||||
# LLM_BACKEND=openai
|
||||
# OPENAI_BASE_URL=http://marketplace-mainnet-chatmock:8000 # or https://api.deepseek.com
|
||||
# OPENAI_TIMEOUT_MS=180000
|
||||
|
||||
# OPENAI_API_KEY — optional bearer token. When set, the supplier forwards it
|
||||
# as `Authorization: Bearer <key>` on every /v1/chat/completions request.
|
||||
# Leave empty for endpoints that don't require auth (ChatMock, other local
|
||||
# proxies). REQUIRED for hosted APIs (DeepSeek, OpenAI). SECRET.
|
||||
# OPENAI_API_KEY=
|
||||
|
||||
# ─── Optional (not in supplier/src/config.ts; consumed by index.ts boot) ──
|
||||
# Pre-derived public-key + pkh + bech32 address. M1-D will derive these from
|
||||
# SUPPLIER_PRIV_KEY_HEX automatically; until then index.ts reads them from
|
||||
|
||||
@@ -65,6 +65,12 @@ export interface SupplierConfig {
|
||||
ollamaUrl: string;
|
||||
/** Empty string when capabilityKind="tts" or llmBackend="ollama". */
|
||||
openaiBaseUrl: string;
|
||||
/**
|
||||
* Bearer token forwarded as `Authorization: Bearer <key>` when llmBackend="openai".
|
||||
* Empty string means no header is sent (correct for ChatMock and other localhost
|
||||
* proxies). Required for hosted OpenAI-compatible APIs (DeepSeek, OpenAI, etc.).
|
||||
*/
|
||||
openaiApiKey: string;
|
||||
/** Empty string when capabilityKind="chat". */
|
||||
piperUrl: string;
|
||||
advertRef: AdvertRef;
|
||||
@@ -193,6 +199,8 @@ export function loadConfig(env: Record<string, string | undefined>): SupplierCon
|
||||
openaiTimeoutMs = Number(openaiTimeoutStr);
|
||||
}
|
||||
|
||||
const openaiApiKey = env.OPENAI_API_KEY ?? "";
|
||||
|
||||
const piperTimeoutStr = env.PIPER_TIMEOUT_MS;
|
||||
let piperTimeoutMs = 120_000;
|
||||
if (piperTimeoutStr !== undefined && piperTimeoutStr !== "") {
|
||||
@@ -212,6 +220,7 @@ export function loadConfig(env: Record<string, string | undefined>): SupplierCon
|
||||
ogmiosUrl,
|
||||
ollamaUrl,
|
||||
openaiBaseUrl,
|
||||
openaiApiKey,
|
||||
piperUrl,
|
||||
advertRef,
|
||||
networkId,
|
||||
|
||||
@@ -91,6 +91,7 @@ export async function runChatJob(params: RunChatJobParams): Promise<void> {
|
||||
model: advert.model,
|
||||
messages: requestBody.messages,
|
||||
timeoutMs: deps.config.openaiTimeoutMs,
|
||||
apiKey: deps.config.openaiApiKey,
|
||||
});
|
||||
} else {
|
||||
inference = await ollama.callOllama({
|
||||
|
||||
@@ -33,6 +33,12 @@ export interface CallOpenAiParams {
|
||||
model: string;
|
||||
messages: ChatMessage[];
|
||||
timeoutMs: number;
|
||||
/**
|
||||
* Optional bearer token. When non-empty, an `authorization: Bearer <apiKey>`
|
||||
* header is sent. Omit (or pass "") for endpoints that don't require auth,
|
||||
* e.g. the ChatMock localhost proxy.
|
||||
*/
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
export interface OpenAiResult {
|
||||
@@ -62,10 +68,15 @@ function isAbortError(err: unknown): boolean {
|
||||
}
|
||||
|
||||
export async function callOpenAi(params: CallOpenAiParams): Promise<OpenAiResult> {
|
||||
const { baseUrl, model, messages, timeoutMs } = params;
|
||||
const { baseUrl, model, messages, timeoutMs, apiKey } = params;
|
||||
const url = `${baseUrl}/v1/chat/completions`;
|
||||
const body = JSON.stringify({ model, messages, stream: false });
|
||||
|
||||
const headers: Record<string, string> = { "content-type": "application/json" };
|
||||
if (apiKey) {
|
||||
headers.authorization = `Bearer ${apiKey}`;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
@@ -74,7 +85,7 @@ export async function callOpenAi(params: CallOpenAiParams): Promise<OpenAiResult
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
headers,
|
||||
body,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
@@ -102,6 +102,39 @@ describe("callOpenAi() — request shape", () => {
|
||||
const body = JSON.parse(init.body as string);
|
||||
expect(body.stream).toBe(false);
|
||||
});
|
||||
|
||||
it("omits authorization header when apiKey is not provided (ChatMock path)", async () => {
|
||||
await callOpenAi({ baseUrl: BASE_URL, model: MODEL, messages: MESSAGES, timeoutMs: TIMEOUT_MS });
|
||||
const [, init] = (global.fetch as ReturnType<typeof vi.fn>).mock.calls[0] as [string, RequestInit];
|
||||
const headers = init.headers as Record<string, string>;
|
||||
expect(headers.authorization).toBeUndefined();
|
||||
});
|
||||
|
||||
it("sends authorization: Bearer <apiKey> when apiKey is provided (DeepSeek/OpenAI path)", async () => {
|
||||
await callOpenAi({
|
||||
baseUrl: BASE_URL,
|
||||
model: MODEL,
|
||||
messages: MESSAGES,
|
||||
timeoutMs: TIMEOUT_MS,
|
||||
apiKey: "sk-test-deepseek-key",
|
||||
});
|
||||
const [, init] = (global.fetch as ReturnType<typeof vi.fn>).mock.calls[0] as [string, RequestInit];
|
||||
const headers = init.headers as Record<string, string>;
|
||||
expect(headers.authorization).toBe("Bearer sk-test-deepseek-key");
|
||||
});
|
||||
|
||||
it("omits authorization header when apiKey is empty string", async () => {
|
||||
await callOpenAi({
|
||||
baseUrl: BASE_URL,
|
||||
model: MODEL,
|
||||
messages: MESSAGES,
|
||||
timeoutMs: TIMEOUT_MS,
|
||||
apiKey: "",
|
||||
});
|
||||
const [, init] = (global.fetch as ReturnType<typeof vi.fn>).mock.calls[0] as [string, RequestInit];
|
||||
const headers = init.headers as Record<string, string>;
|
||||
expect(headers.authorization).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Happy path ─────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user