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:
parent
8e53dee03c
commit
b8854e4e12
416
docs/grpc-client-guide.md
Normal file
416
docs/grpc-client-guide.md
Normal 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
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user