servicegenai

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 6, 2026 License: AGPL-3.0 Imports: 0 Imported by: 0

README

GenAI service

Every AI generation on the platform runs here: the call to the provider, the record that survives a crash without paying twice, and the ledger of what it cost and who it was for.

X (formerly Twitter) Follow Discord


GitHub go.mod Go version GitHub repo file or directory count GitHub code size in bytes

GitHub Actions Workflow Status codecov

Coverage graph

Under construction. This repository currently holds the service scaffold. The generation API, the provider adapter and the cost ledger are being built — see a-novel/.github#245 for the plan and the task order.

What it does

A generation takes minutes, costs real money, and must never be paid for twice. A caller submits what it wants generated; this service picks the model, calls the provider, survives its own restart without re-billing, records exactly what was consumed and what it cost, and hands back the result.

Callers keep their domain and lose their worker. Prompt assembly stays with them — the text and the output schema arrive already built — but nothing about talking to a provider, recovering a crashed call, counting tokens or pricing them is written twice.

Three things shape the contract:

Idempotency is mandatory. Every submission carries a key, and a replay attaches to the work already in flight rather than starting a second, separately billed one. In a service whose only workload is a priced call, an unkeyed submission is a bug the API refuses rather than a default it tolerates.

A crash re-attaches instead of re-paying. The provider's own identifier for an in-flight operation is recorded the moment the call starts. A restarted process resumes that operation; it does not begin a new one.

Cost is recorded, not inferred. The provider, the model and the token breakdown are not enough to reconstruct a bill, because prices change. Each attempt writes a ledger row carrying the unit prices in force at the time and the resulting amount, so what a call cost stays true no matter what the price becomes later. Each row also carries the purpose it was spent on, because the platform does not monetize every AI feature the same way.

The surface is gRPC only. Callers are other services on the internal network, so there is no browser client and no REST API to keep in step.

Deploying

The service runs as published OCI images plus a PostgreSQL database. The server is stateless, so it scales to as many replicas as you need; all state lives in Postgres.

OpenTofu modules are the planned canonical deployment path. Until they land, deploy the images with any container orchestrator — the composition below is the reference for which images to run, how they wire together, and the environment they expect.

Image Role
service-genai/grpc The generation API. Internal network only.
service-genai/migrations One-shot schema migration job; runs to completion before the server starts.
service-genai/database Pre-tuned PostgreSQL image with pg_cron — or bring your own Postgres.
service-genai/standalone-grpc Server plus migrations in one image. Local development only.

Pin every image to the same release tag — see the latest release.

services:
  postgres-genai:
    image: ghcr.io/a-novel/service-genai/database:v0.4.0
    networks: [api]
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_USER: postgres
      POSTGRES_DB: postgres
      POSTGRES_HOST_AUTH_METHOD: scram-sha-256
      POSTGRES_INITDB_ARGS: --auth=scram-sha-256
    volumes:
      - genai-postgres-data:/var/lib/postgresql/

  migrations-genai:
    image: ghcr.io/a-novel/service-genai/migrations:v0.4.0
    depends_on:
      postgres-genai: { condition: service_healthy }
    environment:
      POSTGRES_DSN: "postgres://postgres:postgres@postgres-genai:5432/postgres?sslmode=disable"
    networks: [api]

  service-genai:
    image: ghcr.io/a-novel/service-genai/grpc:v0.4.0
    ports: ["${SERVICE_GENAI_GRPC_PORT}:8080"] # the container always listens on 8080
    depends_on:
      postgres-genai: { condition: service_healthy }
      migrations-genai: { condition: service_completed_successfully }
    environment:
      POSTGRES_DSN: "postgres://postgres:postgres@postgres-genai:5432/postgres?sslmode=disable"
    networks: [api]

networks:
  api:

volumes:
  genai-postgres-data:
Configuration

Every variable is read from the process environment. Names can be globally prefixed with SERVICE_GENAI_ENV_PREFIX, which avoids collisions when another project embeds this service.

Name Description Images
POSTGRES_DSN PostgreSQL connection string. Required. all
Optional configuration (gRPC, connection pool, OpenTelemetry)

gRPC server:

Name Description Default
GRPC_PORT Port the server listens on. 8080
GRPC_PING Refresh interval for the server's internal health check. 5s

Database connection pool (server images). The limits are per process, so the database's max_connections has to cover every replica plus the migration job; the stock postgres default is 100.

Name Description Default
POSTGRES_MAX_OPEN_CONNS Maximum open connections to the database. 20
POSTGRES_MAX_IDLE_CONNS Maximum connections kept open while idle. 20

Logs and tracing — OpenTelemetry supports a stdout and a Google Cloud exporter (all server images):

Name Description Default
OTEL Enable OTel tracing; the variables below pick the exporter. false
GCLOUD_PROJECT_ID Google Cloud project ID. When set, switches the OTel exporter to GCP.
APP_NAME Application name attached to traces and logs. service-genai

Using the client package

The Go client is what a consuming service imports. The snippet below is the minimum viable call; the full surface is what your editor's intellisense and pkg.go.dev are for.

go get github.com/a-novel/service-genai
package main

import (
	"context"
	"log"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"

	servicegenai "github.com/a-novel/service-genai/pkg/go"
)

func main() {
	ctx := context.Background()

	// In production, swap insecure.NewCredentials() for a TLS or mTLS credential — the
	// server has no application-layer auth, so transport security is the only thing
	// protecting it from a network adversary.
	client, err := servicegenai.NewClient(
		"service-genai:8080",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	)
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	resp, err := client.Status(ctx, &servicegenai.StatusRequest{})
	if err != nil {
		log.Fatal(err)
	}

	log.Printf("postgres: %s", resp.GetPostgres().GetStatus())
}

Running locally

The standalone-grpc image bundles the migration job with the server, so a single container brings the service up against an empty database. It is a development convenience: a production deployment runs migrations as their own job, so a failed migration stops the rollout instead of restarting a server.

a-novel run start service-genai/grpc
eval "$(a-novel run env service-genai)"

grpcurl --plaintext localhost:${SERVICE_GENAI_GRPC_PORT} list

Working on the code itself starts with CONTRIBUTING.md.

Contributing

Platform setup and the day-to-day commands live in the developer onboarding guide. What is specific to this service is in CONTRIBUTING.md.

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
cmd
grpc command
Command grpc serves the generation API.
Command grpc serves the generation API.
migrations command
Command migrations applies the service's database schema migrations and exits.
Command migrations applies the service's database schema migrations and exits.
internal
config
Package config assembles the runtime configuration for the service: the typed structs the application reads, and the default preset that populates them from the environment.
Package config assembles the runtime configuration for the service: the typed structs the application reads, and the default preset that populates them from the environment.
config/configtest
Package configtest holds configuration presets shared across the service's integration tests.
Package configtest holds configuration presets shared across the service's integration tests.
config/env
Package env reads and parses the service's configuration from environment variables, exposing each setting as a typed, ready-to-use value.
Package env reads and parses the service's configuration from environment variables, exposing each setting as a typed, ready-to-use value.
dao
lib
Package lib holds the provider adapter: the one place in the platform that talks to a generative AI provider.
Package lib holds the provider adapter: the one place in the platform that talks to a generative AI provider.
models/migrations
Package migrations holds the service's database schema as an ordered set of SQL migration files, embedded into the binary so the schema ships with the service.
Package migrations holds the service's database schema as an ordered set of SQL migration files, embedded into the binary so the schema ships with the service.
pkg
go

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL