# 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 # 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 -f deploy/Dockerfile.deploy deploy image: localhost/fc-llm-bridge:v202604300022 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" - name: FlowerCore__LlmBridge__UtilModel value: "qwen2.5:1.5b" - name: FlowerCore__LlmBridge__EmbedModel value: "nomic-embed-text" # 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