Pods in this cluster inherit ndots=5. External FQDNs with <5 dots (like api.anthropic.com) are expanded through the search path first, and the 4th suffix `api.anthropic.com.iamworkin.lan` matches CoreDNS' `template IN A iamworkin.lan` wildcard — resolves to Traefik VIP 10.0.56.200. TLS connect lands on Traefik's default cert and the AnthropicClient rejects with RemoteCertificateNameMismatch/RemoteCertificateChainErrors. Setting ndots=2 makes the resolver try the bare FQDN first (3 dots in api.anthropic.com), so the search path never fires. Reference: memory feedback_coredns_ndots_template_collision. Wider follow-up: the CoreDNS template plugin should add fallthrough for external public suffixes, so every FC service calling external HTTPS APIs stops hitting this trap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
277 lines
9.5 KiB
YAML
277 lines
9.5 KiB
YAML
# FlowerCore.LlmBridge — OpenAI-compatible bridge for Agent Zero.
|
|
# Routes through FlowerCore.Shared.Chat (ILlmProviderClient) with budget
|
|
# enforcement, response caching, and tier-based model routing. Lets Agent
|
|
# Zero (Python) reach Anthropic and Ollama providers without re-implementing
|
|
# the C# budget/cache/router primitives.
|
|
#
|
|
# Design: FlowerCore.Notes/docs/ai-agents/agent-zero-anthropic-bridge.md
|
|
# ADR: FlowerCore.Notes/ARCHITECTURE.md (ADR-088)
|
|
#
|
|
# Deployment order (see bluejay-infra/README.md):
|
|
# 1. pfSense DNS override for fc-llm-bridge.iamworkin.lan -> 10.0.56.200
|
|
# (REQUIRED before this is applied — cert-manager HTTP-01 will silently
|
|
# fail for ~2h backoff otherwise). Run scripts/pfsense-add-dns-overrides.py.
|
|
# 2. 1Password items `Claude API Key` (already exists) and
|
|
# `FC LLM Bridge API Keys` (create when first non-dev environment comes up).
|
|
# 3. Build + import image: localhost/fc-llm-bridge:v<YYYYMMDD><HHMM>
|
|
# Import to rke2-server, rke2-agent1, rke2-agent2 via ctr images import.
|
|
# 4. Bump the image tag below and git push; ArgoCD ApplicationSet picks up.
|
|
# 5. Flip Agent Zero chat.openai.base_url to https://fc-llm-bridge.iamworkin.lan/v1
|
|
# and api_key to the op://IAmWorkin/FC LLM Bridge API Keys/agent-zero-k8s value.
|
|
---
|
|
apiVersion: v1
|
|
kind: Namespace
|
|
metadata:
|
|
name: fc-llm-bridge
|
|
labels:
|
|
app.kubernetes.io/part-of: flowercore
|
|
---
|
|
# Claude (Anthropic) API key — shared across FC services.
|
|
# Existing 1Password item. `credential` field -> Secret `anthropic-api-key`.
|
|
apiVersion: onepassword.com/v1
|
|
kind: OnePasswordItem
|
|
metadata:
|
|
name: anthropic-api-key
|
|
namespace: fc-llm-bridge
|
|
spec:
|
|
itemPath: "vaults/IAmWorkin/items/Claude API Key"
|
|
---
|
|
# Per-consumer API keys for the bridge itself.
|
|
# NEW 1Password item — see apps/fc-llm-bridge/README.md for the field layout
|
|
# to create before first apply. Fields become Secret keys of the same name:
|
|
# agent-zero-ws, agent-zero-k8s, spare-1, spare-2
|
|
apiVersion: onepassword.com/v1
|
|
kind: OnePasswordItem
|
|
metadata:
|
|
name: fc-llm-bridge-api-keys
|
|
namespace: fc-llm-bridge
|
|
spec:
|
|
itemPath: "vaults/IAmWorkin/items/FC LLM Bridge API Keys"
|
|
---
|
|
apiVersion: v1
|
|
kind: PersistentVolumeClaim
|
|
metadata:
|
|
name: fc-llm-bridge-data
|
|
namespace: fc-llm-bridge
|
|
spec:
|
|
accessModes:
|
|
- ReadWriteOnce
|
|
storageClassName: longhorn
|
|
resources:
|
|
requests:
|
|
storage: 2Gi
|
|
---
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: fc-llm-bridge
|
|
namespace: fc-llm-bridge
|
|
labels:
|
|
app.kubernetes.io/name: fc-llm-bridge
|
|
app.kubernetes.io/part-of: flowercore
|
|
spec:
|
|
replicas: 1
|
|
revisionHistoryLimit: 3
|
|
strategy:
|
|
type: Recreate
|
|
selector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: fc-llm-bridge
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app.kubernetes.io/name: fc-llm-bridge
|
|
app.kubernetes.io/part-of: flowercore
|
|
annotations:
|
|
prometheus.io/scrape: "true"
|
|
prometheus.io/port: "8080"
|
|
prometheus.io/path: "/metrics"
|
|
spec:
|
|
securityContext:
|
|
fsGroup: 1654
|
|
fsGroupChangePolicy: OnRootMismatch
|
|
containers:
|
|
- name: web
|
|
# Placeholder tag — bump to the image you built + imported to every
|
|
# RKE2 node before applying. Build with:
|
|
# dotnet.exe publish -c Release -o deploy/app \
|
|
# src/FlowerCore.LlmBridge.Web/FlowerCore.LlmBridge.Web.csproj
|
|
# podman build -t localhost/fc-llm-bridge:v<tag> -f deploy/Dockerfile.deploy deploy
|
|
image: localhost/fc-llm-bridge:v202604231449
|
|
imagePullPolicy: Never
|
|
ports:
|
|
- containerPort: 8080
|
|
name: http
|
|
env:
|
|
- name: ASPNETCORE_URLS
|
|
value: "http://+:8080"
|
|
- name: ASPNETCORE_ENVIRONMENT
|
|
value: "Production"
|
|
- name: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT
|
|
value: "false"
|
|
# SQLite (budget ledger + response cache + data-protection keys)
|
|
- name: FlowerCore__LlmBridge__SqliteConnectionString
|
|
value: "Data Source=/data/llm-bridge.db"
|
|
- name: FlowerCore__LlmBridge__DefaultTenantId
|
|
value: "default"
|
|
- name: FlowerCore__LlmBridge__DefaultAppName
|
|
value: "agent-zero"
|
|
# Per-consumer API keys — from OnePasswordItem fc-llm-bridge-api-keys.
|
|
# Each field becomes a Secret key of the same name. The key-name
|
|
# lands in the auth principal's `fc.app` claim for ledger scoping.
|
|
- name: FlowerCore__LlmBridge__ApiKeys__agent-zero-ws
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: fc-llm-bridge-api-keys
|
|
key: agent-zero-ws
|
|
optional: true
|
|
- name: FlowerCore__LlmBridge__ApiKeys__agent-zero-k8s
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: fc-llm-bridge-api-keys
|
|
key: agent-zero-k8s
|
|
optional: true
|
|
- name: FlowerCore__LlmBridge__ApiKeys__spare-1
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: fc-llm-bridge-api-keys
|
|
key: spare-1
|
|
optional: true
|
|
- name: FlowerCore__LlmBridge__ApiKeys__spare-2
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: fc-llm-bridge-api-keys
|
|
key: spare-2
|
|
optional: true
|
|
# Shared.Chat — Ollama (edge1 Pi 5 + AI HAT+, matches bridge default)
|
|
- name: FlowerCore__Chat__OllamaBaseUrl
|
|
value: "http://10.0.57.17:11434"
|
|
- name: FlowerCore__Chat__HttpTimeout
|
|
value: "00:05:00"
|
|
# Shared.Chat — Anthropic
|
|
- name: FlowerCore__Chat__Anthropic__Enabled
|
|
value: "true"
|
|
- name: FlowerCore__Chat__Anthropic__ApiKey
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: anthropic-api-key
|
|
key: password
|
|
- name: FlowerCore__Chat__Anthropic__OrganizationId
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: anthropic-api-key
|
|
key: organization_id
|
|
optional: true
|
|
- name: FlowerCore__Chat__Anthropic__BaseUrl
|
|
value: "https://api.anthropic.com"
|
|
- name: FlowerCore__Chat__Anthropic__DefaultModel
|
|
value: "claude-sonnet-4-6"
|
|
- name: FlowerCore__Chat__Anthropic__AnthropicVersion
|
|
value: "2023-06-01"
|
|
- name: FlowerCore__Chat__Anthropic__Timeout
|
|
value: "00:05:00"
|
|
resources:
|
|
requests:
|
|
cpu: 100m
|
|
memory: 256Mi
|
|
limits:
|
|
cpu: 1000m
|
|
memory: 768Mi
|
|
volumeMounts:
|
|
- name: data
|
|
mountPath: /data
|
|
- name: tmp
|
|
mountPath: /tmp
|
|
- name: app-data
|
|
mountPath: /app/data
|
|
securityContext:
|
|
runAsNonRoot: true
|
|
runAsUser: 1654
|
|
runAsGroup: 1654
|
|
allowPrivilegeEscalation: false
|
|
readOnlyRootFilesystem: true
|
|
capabilities:
|
|
drop:
|
|
- ALL
|
|
# tcpSocket probes: the app runs ApiKeyAuthMiddleware. /healthz is
|
|
# registered as anonymous via AuthExemptPaths but tcpSocket avoids any
|
|
# future accidental middleware ordering regression
|
|
# (memory: feedback_k8s_probes_behind_auth_middleware).
|
|
readinessProbe:
|
|
tcpSocket:
|
|
port: 8080
|
|
initialDelaySeconds: 5
|
|
periodSeconds: 10
|
|
livenessProbe:
|
|
tcpSocket:
|
|
port: 8080
|
|
initialDelaySeconds: 15
|
|
periodSeconds: 30
|
|
# Lower ndots so external FQDNs like api.anthropic.com are tried BEFORE
|
|
# the ndots:5 default expands them through the cluster search path, which
|
|
# includes iamworkin.lan. CoreDNS has a `template IN A iamworkin.lan`
|
|
# wildcard that answers `api.anthropic.com.iamworkin.lan` with the
|
|
# Traefik VIP, which then serves a TRAEFIK-DEFAULT-CERT TLS cert and
|
|
# breaks egress to the real Anthropic API (memory:
|
|
# feedback_coredns_ndots_template_collision, generalized to external DNS).
|
|
dnsConfig:
|
|
options:
|
|
- name: ndots
|
|
value: "2"
|
|
volumes:
|
|
- name: data
|
|
persistentVolumeClaim:
|
|
claimName: fc-llm-bridge-data
|
|
- name: tmp
|
|
emptyDir: {}
|
|
# The Dockerfile `WORKDIR /app` pairs with the default
|
|
# SqliteConnectionString "Data Source=data/llm-bridge.db" (relative).
|
|
# The env var above overrides to /data, so /app/data can be emptyDir.
|
|
- name: app-data
|
|
emptyDir: {}
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: fc-llm-bridge
|
|
namespace: fc-llm-bridge
|
|
spec:
|
|
selector:
|
|
app.kubernetes.io/name: fc-llm-bridge
|
|
ports:
|
|
- port: 8080
|
|
targetPort: 8080
|
|
name: http
|
|
---
|
|
apiVersion: cert-manager.io/v1
|
|
kind: Certificate
|
|
metadata:
|
|
name: fc-llm-bridge-cert
|
|
namespace: fc-llm-bridge
|
|
spec:
|
|
secretName: fc-llm-bridge-tls
|
|
issuerRef:
|
|
name: step-ca-acme
|
|
kind: ClusterIssuer
|
|
dnsNames:
|
|
- fc-llm-bridge.iamworkin.lan
|
|
duration: 720h
|
|
renewBefore: 240h
|
|
---
|
|
apiVersion: traefik.io/v1alpha1
|
|
kind: IngressRoute
|
|
metadata:
|
|
name: fc-llm-bridge
|
|
namespace: fc-llm-bridge
|
|
spec:
|
|
entryPoints:
|
|
- websecure
|
|
routes:
|
|
- match: Host(`fc-llm-bridge.iamworkin.lan`)
|
|
kind: Rule
|
|
services:
|
|
- name: fc-llm-bridge
|
|
port: 8080
|
|
tls:
|
|
secretName: fc-llm-bridge-tls
|