Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ PORT=3000
NODE_ENV=development

# AI Service Configuration
# AI provider selection: "openrouter" (default) or "ollama"
# AI provider selection: "openrouter" (default), "ollama", or "mock" (local/demo only)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass AI_PROVIDER through Compose

When a local/demo user copies this env template, sets AI_PROVIDER=mock, and starts the stack with Docker Compose, the gateway container still will not see the setting because docker-compose.yml only lists the variables that get placed in the container environment and omits AI_PROVIDER; Docker’s Compose docs describe the environment attribute as what sets container environment variables. In that scenario the gateway falls back to openrouter and fails startup without OPENROUTER_API_KEY, so please add AI_PROVIDER (and any provider-specific mock/ollama variables as needed) to the gateway service environment.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass AI_PROVIDER through Compose

When a developer follows this new AI_PROVIDER=mock option with docker-compose up, Compose still only passes OPENROUTER_API_KEY and OPENROUTER_MODEL into the gateway (docker-compose.yml lines 18-20), so the container never sees AI_PROVIDER=mock, defaults back to OpenRouter, and fails startup without an OpenRouter key. Please add AI_PROVIDER to the gateway environment passthrough so the documented offline/demo mode works in the Compose stack too.

Useful? React with 👍 / 👎.

AI_PROVIDER=openrouter

# OpenRouter Configuration (when AI_PROVIDER=openrouter)
Expand Down
4 changes: 2 additions & 2 deletions gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ Common optional variables:
| Variable | Default | Notes |
| --- | --- | --- |
| `PORT` | `3000` | Gateway listen port. |
| `AI_PROVIDER` | `openrouter` | Supported values: `openrouter`, `ollama`. |
| `AI_PROVIDER` | `openrouter` | Supported values: `openrouter`, `ollama`, `mock` (local/demo only). |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Document mock in root setup docs

When readers follow the root README instead of gateway/README.md, the env table still says AI_PROVIDER is only openrouter by default or ollama for local experiments (README.md:319), so the newly added local/demo mock path is not discoverable from the main setup flow. Please update the root README alongside this gateway-specific entry to keep the documented supported providers aligned. Nice addition overall; this just avoids setup confusion.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update root provider docs for mock mode

Adding mock here leaves the top-level README's architecture/configuration sections still describing only OpenRouter/Ollama (README.md lines 69 and 319-322), so users following the main setup docs won't discover the new offline provider or that an OpenRouter key is unnecessary in mock mode. Since this PR changes the supported gateway configuration, please update the root docs alongside this gateway-specific table.

Useful? React with 👍 / 👎.

| `OPENROUTER_MODEL` | `z-ai/glm-4.5-air:free` in code/docs unless overridden | OpenRouter model. |
| `OPENROUTER_URL` | `https://openrouter.ai/api/v1/chat/completions` provider default | Used by tests and custom OpenRouter-compatible endpoints. |
| `OLLAMA_URL` | `http://localhost:11434` | Used when `AI_PROVIDER=ollama`. |
Expand Down Expand Up @@ -129,7 +129,7 @@ cd gateway
RECEIPT_STORE=memory CACHE_ENABLED=false go run .
```

The verifier must be reachable at `VERIFIER_URL` for signed requests. OpenRouter startup requires `OPENROUTER_API_KEY` unless `AI_PROVIDER=ollama`.
The verifier must be reachable at `VERIFIER_URL` for signed requests. OpenRouter startup requires `OPENROUTER_API_KEY` unless `AI_PROVIDER` is set to `ollama` or `mock`.

## Testing

Expand Down
2 changes: 2 additions & 0 deletions gateway/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ func CacheMiddleware() gin.HandlerFunc {
if model == "" {
model = "llama2"
}
} else if os.Getenv("AI_PROVIDER") == "mock" {
model = "mock"
} else if model == "" {
model = "z-ai/glm-4.5-air:free"
}
Expand Down
16 changes: 16 additions & 0 deletions gateway/internal/ai/mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package ai

import "context"

// MockProvider implements the Provider interface for local/demo testing
type MockProvider struct{}

// NewMockProvider creates a new MockProvider instance
func NewMockProvider() *MockProvider {
return &MockProvider{}
}

// Generate returns a deterministic mock summary response
func (p *MockProvider) Generate(ctx context.Context, text string) (string, error) {
return "This is a deterministic mock summary of the input text for local/demo testing.", nil
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
2 changes: 2 additions & 0 deletions gateway/internal/ai/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ func NewProvider() (Provider, error) {
return NewOpenRouterProvider(), nil
case "ollama":
return NewOllamaProvider(), nil
case "mock":
return NewMockProvider(), nil
Comment on lines +29 to +33

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Block mock provider in production

Because mock is described as local/demo-only but is accepted unconditionally here, a production deployment with AI_PROVIDER=mock will start cleanly, /readyz reports the AI provider as ok, and paid summarize requests return the canned mock response instead of contacting an AI provider. Please gate this provider to development/test environments (or fail startup when production mode selects it) so a misconfigured paid gateway cannot charge users for fake summaries.

Useful? React with 👍 / 👎.

Comment on lines +29 to +33

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Block mock provider in production

Because the mock provider is documented as local/demo-only but this branch accepts AI_PROVIDER=mock unconditionally, any production deploy that carries this env var (for example from a copied demo .env) will pass startup and /readyz while serving the deterministic fake summary for paid requests. Consider rejecting mock when NODE_ENV=production or requiring an explicit dev-only override so this test provider cannot be enabled accidentally in a real deployment.

Useful? React with 👍 / 👎.

default:
return nil, fmt.Errorf("unsupported AI provider: %s", providerType)
}
Expand Down
25 changes: 25 additions & 0 deletions gateway/internal/ai/provider_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ai

import (
"context"
"testing"
)

Expand Down Expand Up @@ -29,6 +30,12 @@ func TestNewProvider(t *testing.T) {
wantType: "*ai.OllamaProvider",
wantErr: false,
},
{
name: "mock provider",
providerType: "mock",
wantType: "*ai.MockProvider",
wantErr: false,
},
{
name: "unsupported provider",
providerType: "invalid",
Expand Down Expand Up @@ -70,6 +77,10 @@ func TestNewProvider(t *testing.T) {
if _, ok := provider.(*OllamaProvider); !ok {
t.Errorf("NewProvider() returned %T, want *OllamaProvider", provider)
}
case "*ai.MockProvider":
if _, ok := provider.(*MockProvider); !ok {
t.Errorf("NewProvider() returned %T, want *MockProvider", provider)
}
}
})
}
Expand Down Expand Up @@ -135,3 +146,17 @@ func TestNewOllamaProvider_Defaults(t *testing.T) {
t.Errorf("expected default model 'llama2', got '%s'", provider.model)
}
}

func TestMockProvider(t *testing.T) {
provider := NewMockProvider()
ctx := context.Background()
resp, err := provider.Generate(ctx, "hello world")
if err != nil {
t.Fatalf("expected no error, got %v", err)
}

expected := "This is a deterministic mock summary of the input text for local/demo testing."
if resp != expected {
t.Errorf("expected '%s', got '%s'", expected, resp)
}
}
3 changes: 3 additions & 0 deletions gateway/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1040,6 +1040,9 @@ func handleReadyz(c *gin.Context) {
case "ollama":
aiStatus = checkOllamaHealth()
checks["ollama"] = aiStatus
case "mock":
aiStatus = "ok"
checks["mock"] = aiStatus
}
checks["ai_provider"] = gin.H{
"provider": providerType,
Expand Down
95 changes: 95 additions & 0 deletions gateway/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package main
import (
"bytes"
"encoding/json"
"gateway/internal/ai"
"net/http"
"net/http/httptest"
"os"
"strconv"
"strings"
"testing"
"time"

"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -616,3 +618,96 @@ func TestHandleReadyz_RedisUnreachable(t *testing.T) {
checks := response["checks"].(map[string]interface{})
require.Equal(t, "unreachable", checks["redis"])
}

func TestHandleReadyz_MockProvider(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Setenv("AI_PROVIDER", "mock")
t.Setenv("RECEIPT_STORE", "memory")
t.Setenv("CACHE_ENABLED", "false")

origVerifier := checkVerifierHealth
defer func() {
checkVerifierHealth = origVerifier
}()

checkVerifierHealth = func() string { return "ok" }

r := gin.Default()
r.GET("/readyz", handleReadyz)

req, _ := http.NewRequest(http.MethodGet, "/readyz", nil)
w := httptest.NewRecorder()

r.ServeHTTP(w, req)

require.Equal(t, http.StatusOK, w.Code)

var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)

require.Equal(t, true, response["ready"])

checks := response["checks"].(map[string]interface{})
require.Equal(t, "ok", checks["verifier"])
require.Equal(t, "ok", checks["mock"])

aiProviderCheck := checks["ai_provider"].(map[string]interface{})
require.Equal(t, "mock", aiProviderCheck["provider"])
require.Equal(t, "ok", aiProviderCheck["status"])
}

func TestHandleSummarize_MockProvider(t *testing.T) {
// Set up a verifier that returns valid immediately
verifier := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Write([]byte(`{"is_valid":true, "recovered_address":"0xabc","error":""}`))
}))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
defer verifier.Close()

// Environment
t.Setenv("AI_PROVIDER", "mock")
t.Setenv("VERIFIER_URL", verifier.URL)
t.Setenv("SERVER_WALLET_PRIVATE_KEY", "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset cached receipt key state in mock flow test

Setting SERVER_WALLET_PRIVATE_KEY here is not enough because getServerPrivateKey is guarded by serverPrivateKeyOnce, and earlier receipt tests can cache a missing-key error before this test runs. With shuffled test order (go test -shuffle=1780551100980941842 .), this mock-provider integration test returns 500 during receipt generation instead of 200; use the existing reset helper or reset the key globals around this test so it is order-independent. Nice coverage addition, but this needs isolation to keep CI reliable.

Useful? React with 👍 / 👎.

t.Setenv("RECEIPT_STORE", "memory")
t.Setenv("CACHE_ENABLED", "false")

// Initialize AI provider for this test
var err error
aiProvider, err = ai.NewProvider()
require.NoError(t, err)

// Initialize receipt store for this test
err = initReceiptStore()
require.NoError(t, err)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

gin.SetMode(gin.TestMode)
r := gin.New()
r.POST("/api/ai/summarize", handleSummarize)

// Build a valid request with signature/nonce
reqBody := strings.NewReader(`{"text":"hello world text to summarize"}`)
req, _ := http.NewRequest("POST", "/api/ai/summarize", reqBody)
req.Header.Set("X-402-Signature", "sig")
req.Header.Set("X-402-Nonce", "nonce")
req.Header.Set("X-402-Timestamp", strconv.FormatInt(time.Now().Unix(), 10))
req.Header.Set("Content-Type", "application/json")

w := httptest.NewRecorder()
r.ServeHTTP(w, req)

require.Equal(t, http.StatusOK, w.Code)

var response map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)

// Verify deterministic response
expected := "This is a deterministic mock summary of the input text for local/demo testing."
require.Equal(t, expected, response["result"])

// Check that X-402-Receipt header is set
receiptHeader := w.Header().Get("X-402-Receipt")
require.NotEmpty(t, receiptHeader)
}

Loading