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.
|
# generous. Increase if you see ollama_timeout reasons in logs.
|
||||||
OLLAMA_TIMEOUT_MS=120000
|
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) ──
|
# ─── 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
|
# 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
|
# SUPPLIER_PRIV_KEY_HEX automatically; until then index.ts reads them from
|
||||||
|
|||||||
@@ -65,6 +65,12 @@ export interface SupplierConfig {
|
|||||||
ollamaUrl: string;
|
ollamaUrl: string;
|
||||||
/** Empty string when capabilityKind="tts" or llmBackend="ollama". */
|
/** Empty string when capabilityKind="tts" or llmBackend="ollama". */
|
||||||
openaiBaseUrl: string;
|
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". */
|
/** Empty string when capabilityKind="chat". */
|
||||||
piperUrl: string;
|
piperUrl: string;
|
||||||
advertRef: AdvertRef;
|
advertRef: AdvertRef;
|
||||||
@@ -193,6 +199,8 @@ export function loadConfig(env: Record<string, string | undefined>): SupplierCon
|
|||||||
openaiTimeoutMs = Number(openaiTimeoutStr);
|
openaiTimeoutMs = Number(openaiTimeoutStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openaiApiKey = env.OPENAI_API_KEY ?? "";
|
||||||
|
|
||||||
const piperTimeoutStr = env.PIPER_TIMEOUT_MS;
|
const piperTimeoutStr = env.PIPER_TIMEOUT_MS;
|
||||||
let piperTimeoutMs = 120_000;
|
let piperTimeoutMs = 120_000;
|
||||||
if (piperTimeoutStr !== undefined && piperTimeoutStr !== "") {
|
if (piperTimeoutStr !== undefined && piperTimeoutStr !== "") {
|
||||||
@@ -212,6 +220,7 @@ export function loadConfig(env: Record<string, string | undefined>): SupplierCon
|
|||||||
ogmiosUrl,
|
ogmiosUrl,
|
||||||
ollamaUrl,
|
ollamaUrl,
|
||||||
openaiBaseUrl,
|
openaiBaseUrl,
|
||||||
|
openaiApiKey,
|
||||||
piperUrl,
|
piperUrl,
|
||||||
advertRef,
|
advertRef,
|
||||||
networkId,
|
networkId,
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export async function runChatJob(params: RunChatJobParams): Promise<void> {
|
|||||||
model: advert.model,
|
model: advert.model,
|
||||||
messages: requestBody.messages,
|
messages: requestBody.messages,
|
||||||
timeoutMs: deps.config.openaiTimeoutMs,
|
timeoutMs: deps.config.openaiTimeoutMs,
|
||||||
|
apiKey: deps.config.openaiApiKey,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
inference = await ollama.callOllama({
|
inference = await ollama.callOllama({
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ export interface CallOpenAiParams {
|
|||||||
model: string;
|
model: string;
|
||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
timeoutMs: number;
|
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 {
|
export interface OpenAiResult {
|
||||||
@@ -62,10 +68,15 @@ function isAbortError(err: unknown): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function callOpenAi(params: CallOpenAiParams): Promise<OpenAiResult> {
|
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 url = `${baseUrl}/v1/chat/completions`;
|
||||||
const body = JSON.stringify({ model, messages, stream: false });
|
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 controller = new AbortController();
|
||||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
|
|
||||||
@@ -74,7 +85,7 @@ export async function callOpenAi(params: CallOpenAiParams): Promise<OpenAiResult
|
|||||||
try {
|
try {
|
||||||
response = await fetch(url, {
|
response = await fetch(url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "content-type": "application/json" },
|
headers,
|
||||||
body,
|
body,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -102,6 +102,39 @@ describe("callOpenAi() — request shape", () => {
|
|||||||
const body = JSON.parse(init.body as string);
|
const body = JSON.parse(init.body as string);
|
||||||
expect(body.stream).toBe(false);
|
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 ─────────────────────────────────────────────────────────────
|
// ─── Happy path ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user