Add gRPC client guide and update runner docs for local network

- Add comprehensive gRPC client guide with examples for grpcurl, Python,
  Node.js, Go, and Swift clients including streaming and authentication
- Update macOS runner setup with instructions for connecting to local
  Gitea instance running in Docker on Linux (network config, firewall,
  DNS setup)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mathias Beaulieu-Duncan 2025-12-30 04:40:23 -05:00
parent 8e53dee03c
commit b8854e4e12
2 changed files with 517 additions and 0 deletions

416
docs/grpc-client-guide.md Normal file
View File

@ -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

View File

@ -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 - Admin access to your Gitea instance
- Xcode Command Line Tools installed - 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 ## Step 1: Install Xcode Command Line Tools
```bash ```bash