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
|
- 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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user