Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ A cluster-wide OpenAI-compatible proxy that routes LLM traffic to actual provide
| `llm` | Namespace | Dedicated namespace for LLM infrastructure |
| `litellm-config` | ConfigMap | `config.yaml` with `model_list` (model definitions + routing) |
| `litellm-secrets` | Secret | `LITELLM_MASTER_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY` |
| `litellm` | Deployment | `ghcr.io/berriai/litellm:main-stable`, port 4000 |
| `litellm` | Deployment | `ghcr.io/berriai/litellm:main-v1.82.3`, port 4000 |
| `litellm` | Service | `litellm.llm.svc.cluster.local:4000` |
| `ollama` | Service (ExternalName) | Routes to host Ollama |

Expand Down
12 changes: 12 additions & 0 deletions Dockerfile.reth-erc8004-indexer
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM rust:1.93-bookworm AS builder

WORKDIR /build

COPY reth-erc8004-indexer/Cargo.toml reth-erc8004-indexer/Cargo.toml
COPY reth-erc8004-indexer/src reth-erc8004-indexer/src

RUN cargo build --release --manifest-path reth-erc8004-indexer/Cargo.toml

FROM ghcr.io/paradigmxyz/reth:v1.11.1

COPY --from=builder /build/target/release/reth /usr/local/bin/reth
33 changes: 33 additions & 0 deletions Dockerfile.worker
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
FROM nvidia/cuda:12.4.1-runtime-ubuntu22.04

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
curl \
git \
python3 \
python3-pip \
python3-venv \
&& rm -rf /var/lib/apt/lists/*

# Install uv for running the autoresearch repo environment.
RUN curl -LsSf https://astral.sh/uv/install.sh | sh \
&& mv /root/.local/bin/uv /usr/local/bin/uv

WORKDIR /app

COPY internal/embed/skills/autoresearch-worker/scripts/worker_api.py /app/worker_api.py

ENV DATA_DIR=/data \
AUTORESEARCH_REPO=/data/autoresearch \
EXPERIMENT_TIMEOUT_SECONDS=300 \
TRAIN_COMMAND="uv run train.py"

RUN useradd -m -s /bin/bash worker && mkdir -p /data && chown worker:worker /data

USER worker
VOLUME ["/data"]
EXPOSE 8080

ENTRYPOINT ["python3", "/app/worker_api.py", "serve", "--repo", "/data/autoresearch", "--data-dir", "/data", "--host", "0.0.0.0", "--port", "8080"]
2 changes: 1 addition & 1 deletion cmd/obol/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ func modelSetupCustomCommand(cfg *config.Config) *cli.Command {
Action: func(ctx context.Context, cmd *cli.Command) error {
u := getUI(cmd)
name := cmd.String("name")
endpoint := model.WarnAndStripV1Suffix(cmd.String("endpoint"))
endpoint := cmd.String("endpoint")
modelName := cmd.String("model")
apiKey := cmd.String("api-key")

Expand Down
183 changes: 182 additions & 1 deletion cmd/obol/sell.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package main

import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"os/signal"
"runtime"
Expand Down Expand Up @@ -37,6 +40,7 @@ func sellCommand(cfg *config.Config) *cli.Command {
sellHTTPCommand(cfg),
sellListCommand(cfg),
sellStatusCommand(cfg),
sellProbeCommand(cfg),
sellStopCommand(cfg),
sellDeleteCommand(cfg),
sellPricingCommand(cfg),
Expand Down Expand Up @@ -146,6 +150,10 @@ Examples:
Usage: "SHA-256 of model weights for TEE attestation (required with --tee)",
Sources: cli.EnvVars("OBOL_MODEL_HASH"),
},
&cli.StringFlag{
Name: "provenance-file",
Usage: "Path to JSON file with provenance metadata (e.g. autoresearch experiment results)",
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
name := cmd.Args().First()
Expand Down Expand Up @@ -204,6 +212,16 @@ Examples:
TEEType: teeType,
ModelHash: modelHash,
}

if pf := cmd.String("provenance-file"); pf != "" {
prov, err := loadProvenance(pf)
if err != nil {
return fmt.Errorf("load provenance: %w", err)
}
d.Provenance = prov
fmt.Printf("Loaded provenance: %s (metric %s=%s, params %s)\n",
prov.Framework, prov.MetricName, prov.MetricValue, prov.ParamCount)
}
if priceTable.PerMTok != "" {
d.ApproxTokensPerRequest = schemas.ApproxTokensPerRequest
}
Expand Down Expand Up @@ -379,6 +397,14 @@ Examples:
Name: "register-domains",
Usage: "OASF domains for discovery (e.g. technology/artificial_intelligence)",
},
&cli.StringSliceFlag{
Name: "register-metadata",
Usage: "Additional registration metadata as key=value pairs (repeatable, e.g. gpu=A100-80GB)",
},
&cli.StringFlag{
Name: "provenance-file",
Usage: "Path to JSON file with provenance metadata (e.g. autoresearch experiment results)",
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.NArg() == 0 {
Expand Down Expand Up @@ -422,6 +448,25 @@ Examples:
spec["path"] = path
}

if pf := cmd.String("provenance-file"); pf != "" {
prov, err := loadProvenance(pf)
if err != nil {
return fmt.Errorf("load provenance: %w", err)
}
// Round-trip through JSON to build the map, respecting omitempty tags.
provBytes, err := json.Marshal(prov)
if err != nil {
return fmt.Errorf("marshal provenance: %w", err)
}
var provMap map[string]interface{}
if err := json.Unmarshal(provBytes, &provMap); err != nil {
return fmt.Errorf("unmarshal provenance: %w", err)
}
spec["provenance"] = provMap
fmt.Printf("Loaded provenance: %s (metric %s=%s, params %s)\n",
prov.Framework, prov.MetricName, prov.MetricValue, prov.ParamCount)
}

if cmd.Bool("register") || cmd.String("register-name") != "" {
reg := map[string]interface{}{
"enabled": cmd.Bool("register"),
Expand All @@ -441,6 +486,13 @@ Examples:
if domains := cmd.StringSlice("register-domains"); len(domains) > 0 {
reg["domains"] = domains
}
if metaPairs := cmd.StringSlice("register-metadata"); len(metaPairs) > 0 {
meta, err := parseMetadataPairs(metaPairs)
if err != nil {
return err
}
reg["metadata"] = meta
}
spec["registration"] = reg
}

Expand Down Expand Up @@ -580,6 +632,101 @@ func sellStatusCommand(cfg *config.Config) *cli.Command {
}
}

// ---------------------------------------------------------------------------
// sell probe — send an unauthenticated request to verify 402 payment gate
// ---------------------------------------------------------------------------

func sellProbeCommand(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "probe",
Usage: "Probe a ServiceOffer endpoint to verify it returns 402 pricing",
ArgsUsage: "<name>",
Description: `Sends an unauthenticated request through Traefik to the ServiceOffer's
endpoint and displays the HTTP status code and x402 pricing response.

A 402 response with x402Version=1 confirms the endpoint is live and payment-gated.

Examples:
obol sell probe flow-qwen -n llm
obol sell probe my-api -n default --path /health`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "namespace",
Aliases: []string{"n"},
Usage: "Namespace of the ServiceOffer",
},
&cli.StringFlag{
Name: "path",
Usage: "Subpath to probe (appended to the offer's endpoint)",
Value: "/health",
},
&cli.StringFlag{
Name: "host",
Usage: "Traefik host:port",
Value: "obol.stack:8080",
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
name := cmd.Args().First()
if name == "" {
return fmt.Errorf("name required: obol sell probe <name> -n <ns>")
}
ns := cmd.String("namespace")
if ns == "" {
return fmt.Errorf("namespace required: obol sell probe <name> -n <ns>")
}

// Get the ServiceOffer's endpoint from the CR status.
endpoint, err := kubectlOutput(cfg, "get", "serviceoffers.obol.org", name,
"-n", ns, "-o", "jsonpath={.status.endpoint}")
if err != nil {
return fmt.Errorf("get ServiceOffer %s/%s: %w", ns, name, err)
}
endpoint = strings.TrimSpace(endpoint)
if endpoint == "" {
return fmt.Errorf("ServiceOffer %s/%s has no endpoint (not yet reconciled?)", ns, name)
}

subpath := cmd.String("path")
probeURL := "http://" + cmd.String("host") + endpoint + subpath
fmt.Printf("Probing %s ...\n", probeURL)

req, err := http.NewRequestWithContext(ctx, http.MethodGet, probeURL, nil)
if err != nil {
return fmt.Errorf("create request: %w", err)
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("probe failed: %w", err)
}
defer resp.Body.Close()

body, _ := io.ReadAll(resp.Body)
fmt.Printf("HTTP %d\n", resp.StatusCode)

if resp.StatusCode == http.StatusPaymentRequired {
var pretty bytes.Buffer
if json.Indent(&pretty, body, "", " ") == nil {
fmt.Println(pretty.String())
} else {
fmt.Println(string(body))
}
fmt.Println("\nEndpoint is live and payment-gated.")
return nil
}

if len(body) > 0 {
fmt.Println(string(body))
}
if resp.StatusCode == http.StatusOK {
fmt.Println("\nWarning: endpoint returned 200 (not payment-gated).")
}
return nil
},
}
}

// ---------------------------------------------------------------------------
// sell stop
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1054,6 +1201,18 @@ func valueOrNone(s string) string {
return s
}

func parseMetadataPairs(values []string) (map[string]string, error) {
meta := make(map[string]string, len(values))
for _, raw := range values {
key, value, ok := strings.Cut(raw, "=")
if !ok || strings.TrimSpace(key) == "" {
return nil, fmt.Errorf("invalid --register-metadata value %q: expected key=value", raw)
}
meta[strings.TrimSpace(key)] = strings.TrimSpace(value)
}
return meta, nil
}

func resolvePriceTable(cmd *cli.Command, allowPerHour bool) (schemas.PriceTable, error) {
perRequest := cmd.String("price")
if perRequest == "" {
Expand All @@ -1074,6 +1233,9 @@ func resolvePriceTable(cmd *cli.Command, allowPerHour bool) (schemas.PriceTable,
}
return schemas.PriceTable{PerMTok: perMTok}, nil
case perHour != "":
if _, err := schemas.ApproximateRequestPriceFromPerHour(perHour); err != nil {
return schemas.PriceTable{}, fmt.Errorf("invalid --per-hour value %q: %w", perHour, err)
}
return schemas.PriceTable{PerHour: perHour}, nil
default:
if allowPerHour {
Expand All @@ -1094,7 +1256,11 @@ func formatPriceTableSummary(priceTable schemas.PriceTable) string {
schemas.ApproxTokensPerRequest,
)
case priceTable.PerHour != "":
return fmt.Sprintf("%s USDC/hour", priceTable.PerHour)
return fmt.Sprintf("%s USDC/request (approx from %s USDC/hour @ %d min/request)",
priceTable.EffectiveRequestPrice(),
priceTable.PerHour,
schemas.ApproxMinutesPerRequest,
)
default:
return "0 USDC/request"
}
Expand All @@ -1119,6 +1285,21 @@ func formatInferencePriceSummary(d *inference.Deployment) string {
return fmt.Sprintf("%s USDC/request", d.PricePerRequest)
}

// loadProvenance reads a provenance JSON file and returns the parsed struct.
func loadProvenance(path string) (*inference.Provenance, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read %s: %w", path, err)
}
var prov inference.Provenance
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
if err := dec.Decode(&prov); err != nil {
return nil, fmt.Errorf("parse %s: %w", path, err)
}
return &prov, nil
}

// createHostService creates a headless Service + Endpoints in the cluster
// pointing to the Docker host IP on the given port, so that the cluster can
// route traffic to a host-side inference gateway.
Expand Down
Loading
Loading