diff --git a/docs/grpc-client-guide.md b/docs/grpc-client-guide.md new file mode 100644 index 0000000..c489c7c --- /dev/null +++ b/docs/grpc-client-guide.md @@ -0,0 +1,416 @@ +# gRPC Client Guide + +This guide explains how to connect to and interact with the Apple Intelligence gRPC server from various programming languages. + +## Server Information + +- **Default Host:** `0.0.0.0` (all interfaces) +- **Default Port:** `50051` +- **Protocol:** gRPC over HTTP/2 (plaintext) +- **Service:** `appleintelligence.AppleIntelligence` + +## Available Methods + +| Method | Type | Description | +|--------|------|-------------| +| `Health` | Unary | Check server and model status | +| `Complete` | Unary | Generate a complete response | +| `StreamComplete` | Server Streaming | Generate response with streaming tokens | + +## Proto Definition + +```protobuf +syntax = "proto3"; +package appleintelligence; + +service AppleIntelligence { + rpc Health(HealthRequest) returns (HealthResponse); + rpc Complete(CompletionRequest) returns (CompletionResponse); + rpc StreamComplete(CompletionRequest) returns (stream CompletionChunk); +} + +message HealthRequest {} + +message HealthResponse { + bool healthy = 1; + string model_status = 2; +} + +message CompletionRequest { + string prompt = 1; + optional float temperature = 2; + optional int32 max_tokens = 3; +} + +message CompletionResponse { + string id = 1; + string text = 2; + string finish_reason = 3; +} + +message CompletionChunk { + string id = 1; + string delta = 2; + bool is_final = 3; + optional string finish_reason = 4; +} +``` + +## Testing with grpcurl + +[grpcurl](https://github.com/fullstorydev/grpcurl) is a command-line tool for interacting with gRPC servers. + +### Installation + +```bash +# macOS +brew install grpcurl + +# Linux +go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest +``` + +### Health Check + +```bash +grpcurl -plaintext \ + -proto Sources/Protos/apple_intelligence.proto \ + localhost:50051 \ + appleintelligence.AppleIntelligence/Health +``` + +Response: +```json +{ + "healthy": true, + "modelStatus": "available" +} +``` + +### Complete (Non-Streaming) + +```bash +grpcurl -plaintext \ + -proto Sources/Protos/apple_intelligence.proto \ + -d '{"prompt": "What is 2 + 2?"}' \ + localhost:50051 \ + appleintelligence.AppleIntelligence/Complete +``` + +Response: +```json +{ + "id": "abc123", + "text": "2 + 2 equals 4.", + "finishReason": "stop" +} +``` + +### StreamComplete (Streaming) + +```bash +grpcurl -plaintext \ + -proto Sources/Protos/apple_intelligence.proto \ + -d '{"prompt": "Tell me a short story"}' \ + localhost:50051 \ + appleintelligence.AppleIntelligence/StreamComplete +``` + +Response (multiple chunks): +```json +{"id": "xyz789", "delta": "Once", "isFinal": false} +{"id": "xyz789", "delta": " upon", "isFinal": false} +{"id": "xyz789", "delta": " a", "isFinal": false} +{"id": "xyz789", "delta": " time", "isFinal": false} +... +{"id": "xyz789", "delta": "", "isFinal": true, "finishReason": "stop"} +``` + +## Python Client + +### Installation + +```bash +pip install grpcio grpcio-tools +``` + +### Generate Python Code from Proto + +```bash +python -m grpc_tools.protoc \ + -I. \ + --python_out=. \ + --grpc_python_out=. \ + Sources/Protos/apple_intelligence.proto +``` + +### Basic Client + +```python +import grpc +import apple_intelligence_pb2 as pb +import apple_intelligence_pb2_grpc as rpc + +# Connect to server +channel = grpc.insecure_channel('localhost:50051') +stub = rpc.AppleIntelligenceStub(channel) + +# Health check +health = stub.Health(pb.HealthRequest()) +print(f"Healthy: {health.healthy}") +print(f"Model Status: {health.model_status}") + +# Non-streaming completion +response = stub.Complete(pb.CompletionRequest( + prompt="What is the capital of France?" +)) +print(f"Response: {response.text}") + +# Streaming completion +for chunk in stub.StreamComplete(pb.CompletionRequest( + prompt="Write a haiku about coding" +)): + if chunk.delta: + print(chunk.delta, end='', flush=True) +print() # Newline at end +``` + +### Async Client + +```python +import asyncio +import grpc.aio +import apple_intelligence_pb2 as pb +import apple_intelligence_pb2_grpc as rpc + +async def main(): + async with grpc.aio.insecure_channel('localhost:50051') as channel: + stub = rpc.AppleIntelligenceStub(channel) + + # Streaming with async + async for chunk in stub.StreamComplete(pb.CompletionRequest( + prompt="Explain quantum computing" + )): + if chunk.delta: + print(chunk.delta, end='', flush=True) + print() + +asyncio.run(main()) +``` + +## Node.js / TypeScript Client + +### Installation + +```bash +npm install @grpc/grpc-js @grpc/proto-loader +``` + +### Client + +```javascript +const grpc = require('@grpc/grpc-js'); +const protoLoader = require('@grpc/proto-loader'); + +// Load proto +const packageDef = protoLoader.loadSync('Sources/Protos/apple_intelligence.proto'); +const proto = grpc.loadPackageDefinition(packageDef).appleintelligence; + +// Create client +const client = new proto.AppleIntelligence( + 'localhost:50051', + grpc.credentials.createInsecure() +); + +// Health check +client.Health({}, (err, response) => { + console.log('Healthy:', response.healthy); + console.log('Model Status:', response.modelStatus); +}); + +// Non-streaming completion +client.Complete({ prompt: 'Hello, how are you?' }, (err, response) => { + console.log('Response:', response.text); +}); + +// Streaming completion +const stream = client.StreamComplete({ prompt: 'Tell me a joke' }); +stream.on('data', (chunk) => { + process.stdout.write(chunk.delta); +}); +stream.on('end', () => { + console.log('\nStream ended'); +}); +``` + +## Go Client + +### Installation + +```bash +go install google.golang.org/protobuf/cmd/protoc-gen-go@latest +go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest +``` + +### Generate Go Code + +```bash +protoc --go_out=. --go-grpc_out=. Sources/Protos/apple_intelligence.proto +``` + +### Client + +```go +package main + +import ( + "context" + "fmt" + "io" + "log" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + pb "your-module/appleintelligence" +) + +func main() { + // Connect + conn, err := grpc.Dial("localhost:50051", + grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Fatal(err) + } + defer conn.Close() + + client := pb.NewAppleIntelligenceClient(conn) + ctx := context.Background() + + // Health check + health, _ := client.Health(ctx, &pb.HealthRequest{}) + fmt.Printf("Healthy: %v\n", health.Healthy) + + // Streaming + stream, _ := client.StreamComplete(ctx, &pb.CompletionRequest{ + Prompt: "What is AI?", + }) + + for { + chunk, err := stream.Recv() + if err == io.EOF { + break + } + fmt.Print(chunk.Delta) + } + fmt.Println() +} +``` + +## Swift Client + +### Using grpc-swift + +```swift +import GRPC +import NIO + +let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) +defer { try! group.syncShutdownGracefully() } + +let channel = try GRPCChannelPool.with( + target: .host("localhost", port: 50051), + transportSecurity: .plaintext, + eventLoopGroup: group +) + +let client = Appleintelligence_AppleIntelligenceAsyncClient(channel: channel) + +// Health check +let health = try await client.health(Appleintelligence_HealthRequest()) +print("Healthy: \(health.healthy)") + +// Streaming +for try await chunk in client.streamComplete(Appleintelligence_CompletionRequest.with { + $0.prompt = "Hello!" +}) { + print(chunk.delta, terminator: "") +} +``` + +## Authentication + +If the server is configured with an API key, include it in the metadata: + +### grpcurl + +```bash +grpcurl -plaintext \ + -H "Authorization: Bearer YOUR_API_KEY" \ + -d '{"prompt": "Hello"}' \ + localhost:50051 \ + appleintelligence.AppleIntelligence/Complete +``` + +### Python + +```python +metadata = [('authorization', 'Bearer YOUR_API_KEY')] +response = stub.Complete(request, metadata=metadata) +``` + +### Node.js + +```javascript +const metadata = new grpc.Metadata(); +metadata.add('authorization', 'Bearer YOUR_API_KEY'); +client.Complete({ prompt: 'Hello' }, metadata, callback); +``` + +## Network Access + +### Local Network + +To connect from other devices on your network: + +1. Find the server Mac's IP address: + ```bash + ipconfig getifaddr en0 + ``` + +2. Connect using the IP: + ```bash + grpcurl -plaintext 192.168.1.100:50051 ... + ``` + +### Firewall + +If connections fail, check macOS firewall: +- System Settings → Network → Firewall +- Allow incoming connections for the app + +## Troubleshooting + +### Connection Refused + +- Verify server is running +- Check host and port +- Ensure firewall allows connections + +### Deadline Exceeded + +- Server may be overloaded +- Increase timeout in client +- Check network latency + +### Unauthenticated + +- API key is required but not provided +- API key is incorrect +- Check `Authorization` header format + +### Model Not Available + +- Apple Intelligence is not enabled on the server Mac +- Check System Settings → Apple Intelligence & Siri +- Ensure macOS 26+ and Apple Silicon diff --git a/docs/macos-runner-setup.md b/docs/macos-runner-setup.md index 94ac67a..fb2efb3 100644 --- a/docs/macos-runner-setup.md +++ b/docs/macos-runner-setup.md @@ -8,6 +8,107 @@ This guide explains how to set up a self-hosted Gitea Actions runner on macOS fo - Admin access to your Gitea instance - Xcode Command Line Tools installed +## Network Setup (Local Gitea in Docker on Linux) + +If your Gitea instance is running in Docker on a Linux server on your local network, follow these steps to ensure the Mac runner can connect. + +### 1. Find Your Linux Server's IP Address + +On the Linux server: + +```bash +hostname -I | awk '{print $1}' +# Example output: 192.168.1.50 +``` + +### 2. Ensure Gitea is Accessible + +Verify Gitea's Docker container exposes the correct ports. In your `docker-compose.yml`: + +```yaml +services: + gitea: + image: gitea/gitea:latest + ports: + - "3000:3000" # Web UI + - "22:22" # SSH (optional) + environment: + - GITEA__server__ROOT_URL=http://192.168.1.50:3000/ + - GITEA__server__DOMAIN=192.168.1.50 + - GITEA__server__SSH_DOMAIN=192.168.1.50 +``` + +**Important:** Replace `192.168.1.50` with your Linux server's actual IP. + +If you're using a reverse proxy (Nginx, Traefik), ensure it's configured to accept connections from your local network. + +### 3. Test Connectivity from Mac + +From your Mac, verify you can reach Gitea: + +```bash +# Test web access +curl -I http://192.168.1.50:3000 + +# Or if using a domain name +ping gitea.local +curl -I http://gitea.local:3000 +``` + +### 4. (Optional) Add Local DNS Entry + +For easier access, add the server to your Mac's hosts file: + +```bash +sudo nano /etc/hosts +``` + +Add: +``` +192.168.1.50 gitea.local +``` + +Now you can use `http://gitea.local:3000` instead of the IP address. + +### 5. Linux Firewall Configuration + +Ensure the Linux server's firewall allows connections on port 3000: + +```bash +# UFW (Ubuntu/Debian) +sudo ufw allow 3000/tcp + +# firewalld (CentOS/Fedora) +sudo firewall-cmd --permanent --add-port=3000/tcp +sudo firewall-cmd --reload + +# iptables +sudo iptables -A INPUT -p tcp --dport 3000 -j ACCEPT +``` + +### 6. Enable Gitea Actions + +In your Gitea instance, ensure Actions is enabled: + +1. Edit `app.ini` (usually in `/data/gitea/conf/app.ini` in Docker): + ```ini + [actions] + ENABLED = true + ``` + +2. Or set via environment variable in `docker-compose.yml`: + ```yaml + environment: + - GITEA__actions__ENABLED=true + ``` + +3. Restart Gitea: + ```bash + docker-compose restart gitea + ``` + +--- + ## Step 1: Install Xcode Command Line Tools ```bash