---
# System prepended metadata

title: GPT api
tags: [Coding]

---

# GPT api
## json格式介紹
前面的key一定要是用雙引號`" "`標起來的String
後面的值可以是
| 類型 | 範例 | 說明 |
| --- | --- | --- |
| **字串** | `"王總"` | 必須用雙引號包住 |
| **數字** | `42`、`3.14` | 不用引號，可以是整數或小數 |
| **布林值** | `true`、`false` | 小寫，不能寫成 True/False |
| **null** | `null` | 表示沒有值 |
| **陣列** | `[1, 2, 3]`、`["Java", "SQL"]` | 用中括號，裡面可以放各種值 |
| **物件** | `{ "school": "台大", "year": 2025 }` | 用大括號包住，裡面是 key-value 結構 |

例子
```json
{
    "name": "王總",
    "age": 30,
    "isStudent": false,
    "skills": ["Java", "Python", "SQL"],
    "profile": {
        "school": "台大",
        "major": "資訊工程"
    },
    "messages": [
        {
            "role": "user",
            "content": "你好"
        },
        {
            "role": "assistant",
            "content": "你好！有什麼我可以幫忙的嗎？"
        }
    ]
}
```
說明
```json
{
  "name": "王總",                // 字串
  "age": 30,                     // 數字
  "isStudent": false,           // 布林值
  "skills": [...],              // 陣列（字串）
  "profile": {...},             // 巢狀物件
  "messages": [ {...}, {...} ]  // 陣列裡面是物件
}
```

陣列裡面也可以放不同類別的值 雖然不是很常見
```json
{
  "data": [
    "王總",          // 字串
    42,              // 數字
    true,            // 布林值
    null,            // 空值
    { "a": 1 },      // 物件
    [1, 2, 3]        // 陣列
  ]
}
```
## send_message
```json
POST /v1/chat/completions
{
  "model": "gpt-4o",
  "stream": true,
  "temperature": 0.2,
  "top_p": 0.9,
  "max_tokens": 512,
  "seed": 1234,
  "response_format": {"type":"json_object"},
  "tools": [{
    "type":"function",
    "function":{
      "name":"search_flights",
      "description":"查詢航班",
      "parameters":{
        "type":"object",
        "properties":{
          "from":{"type":"string"},
          "to":{"type":"string"},
          "date":{"type":"string"}
        },
        "required":["from","to","date"]
      }
    }
  }],
  "tool_choice": "auto",
  "messages": [
    {"role":"system","content":"你是一位旅遊助理"},
    {"role":"user","content":"幫我找 5/1 從 TPE 飛 KIX 的機票"}
  ],
  "user": "uid_9876"
}
```
參數 | 型別／預設 | 重點
| - | - | -
model* | string | 指定模型名稱（如 gpt-4o、gpt-4o-mini）
messages* | array | 對話歷史，角色可為 system / user / assistant / tool
temperature | number 0–2，預設 1 | 越高回覆越發散；0 最穩定
top_p | number 0–1，預設 1 | nucleus sampling；與 temperature 二選一調整為主
max_tokens | int | 限制「輸出」token 上限；不設則以模型剩餘 context 為限
stop | string 或 string[](≤4) | 遇到指定字元／字串立即停止生成
presence_penalty | number -2~2，預設 0 | + 促進新主題，- 厭惡離題
frequency_penalty | number -2~2，預設 0 | 抑制重複用詞
logit_bias | {token:int} | 對單一 token 機率微調（-100~100）
seed | int | 固定隨機種子，讓同樣 prompt 可重現
tools | array | 宣告可呼叫的函式/外部工具
tool_choice | "auto" (預設) / "none" / {type:"function",function:{name:"foo"}} | 決定是否、或指定哪一支函式要被呼叫
response_format | {"type":"text"}(預設) / {"type":"json_object"} | 要求輸出純文字或有效 JSON
stream | bool，預設 false | true 時用 SSE 邊產生邊推送
stream_options | {"include_usage":true}(選填) | 串流同時夾帶 usage 統計
user | string | 你的內部使用者 ID，利於濫用追蹤
n | int，預設 1 | 生成幾個 choices；stream 時僅支援 1
logprobs | object | 要求回傳 top-k 機率（僅支援末一 token）
top_logprobs | int 1–20 | 與 logprobs 搭配，指定 k 值

### 語氣調控（temperature & top_p）

先固定 top_p=1，只調 temperature
```
0.0‒0.3 保守、幾乎不創新
0.4‒0.7 最自然、常用區間
0.8‒1.0 創意較多、字數可能變長
>1.0 　天馬行空，注意邏輯錯誤機率上升
```

top-p
把「所有候選 token」依機率由高到底排序，僅保留「累積機率總和 ≥ p」的那一小撮，再在其中用 temperature 洗牌抽籤。
=>這麼做等於先「切掉最冷門、長尾的字」，讓模型寫作時不會忽然跳出怪句子；而 temperature 則負責決定剩下那些字要多隨機。
**越小越容易出現奇怪的字**
**EX:**
![image](https://hackmd.io/_uploads/SJzmnMWlxg.png)

```
  ┌──────────────── ① presence_penalty ─────────────┐
  │   給「已出現過」的字詞 **一次性扣分**，鼓勵新主題      │
  └───────────────────────────────────────────────┘
               ↓
  ┌──────────────── ② frequency_penalty ───────────┐
  │   按「已出現次數」乘權重扣分，抑制重複用語            │
  └───────────────────────────────────────────────┘
               ↓
  ┌──────────────── ③ top-p (nucleus) ─────────────┐
  │   依機率排序，保留累積 ≥ p 的候選字（剪尾）          │
  └───────────────────────────────────────────────┘
               ↓
  ┌──────────────── ④ temperature ─────────────────┐
  │   對剩餘候選重新拉平／尖化機率，再隨機抽籤            │
  └───────────────────────────────────────────────┘
               ↓
            抽到下一個 token
```
| 參數 | 典型範圍 | 調高影響 | 建議用途 |
|------|----------|----------|----------|
| **presence_penalty** | `0 – 2` | ✔ 引入**全新**字詞、話題 | 腦力激盪、劇情走向 |
| **frequency_penalty** | `0 – 2` | ✔ 減少重覆句、口頭禪 | 摘要、客服、技術文 |
| **top_p** | `0.4 – 1` | ✔ 砍掉低機率的冷門字 | 設成1：不裁剪，等同「全池」<br>0.8~1.0 用於自由聊天<br>0.4~0.7 用於摘要、客服<br>控非法用語、縮短回答 |
| **temperature** | `0 – 1.5` | ✔ 提升/降低整體亂度<br>(控制剩下候選字的隨機度) | 設成1：不變，機率照原分佈<br>0.0~0.3 嚴謹、一致<br>0.4~0.8 常態人類語氣<br>>1 創意 |
### 角色
| `role` 值 | 說話者 | 說明 |
| --- | --- | --- |
| `"system"` | 系統/開場設定 | 用來設定助手的行為風格或背景，像是「你是一個日語老師」 |
| `"user"` | 使用者 | 代表你輸入給 GPT 的訊息 |
| `"assistant"` | ChatGPT 回覆的內容 | 代表 GPT 的回答 |

`role` 和 `content` 是用來構成「對話歷史」的基本單位，也就是一組組的訊息。每一則訊息都是一個物件。

**範例：一個完整的對話歷史**
```json
{
  "model": "gpt-4",
  "messages": [
    {
      "role": "system",
      "content": "你是一個有禮貌的 AI 助手。"
    },
    {
      "role": "user",
      "content": "幫我寫一封請假信。"
    },
    {
      "role": "assistant",
      "content": "好的，以下是一封請假信的範例..."
    }
  ]
}
```
-   系統先設定 GPT 的角色風格。
-   使用者提出請求。
-   GPT 依照指令給出回覆。

### call function required
`required` 是 **告訴模型「這些欄位是一定要填的」**，不填就不能執行這個 function。
-   GPT 會努力產生包含 `required` 欄位的 `arguments`    
-   如果使用者說得不清楚，GPT 會「先問問題」來補全 `required` 的資訊再 call function

```json
{
  "name": "create_calendar_event",
  "parameters": {
    "type": "object",
    "properties": {
      "title": { "type": "string" },
      "start_time": { "type": "string" },
      "end_time": { "type": "string" },
      "location": { "type": "string" },
      "description": { "type": "string" }
    },
    "required": ["title", "start_time", "end_time"]
  }
}
```
### item與回傳array格式
`items` 是用來定義「**陣列（list）裡的每一項要長怎樣**」的。

```json
{
  "name": "add_tags_to_event",
  "description": "替事件加上多個標籤",
  "parameters": {
    "type": "object",
    "properties": {
      "event_id": {
        "type": "string",
        "description": "事件的唯一 ID"
      },
      "tags": {
        "type": "array",
        "description": "要加入的標籤清單",
        "items": {
          "type": "string"
        }
      }
    },
    "required": ["event_id", "tags"]
  }
}
```
回傳的樣子
```json
{
  "role": "assistant",
  "function_call": {
    "name": "add_tags_to_event",
    "arguments": { 
      "event_id\": "123",
      "tags": ["會議", "重要", "線上"] 
    }
  }
}
```

#### 二維陣列
```json
{
  "name": "set_meeting_slots",
  "description": "設定多個會議時段（每段都有開始與結束）",
  "parameters": {
    "type": "object",
    "properties": {
      "slots": {
        "type": "array",
        "description": "一組會議時段，每個時段是一個 [start, end] 陣列",
        "items": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "minItems": 2,
          "maxItems": 2
        }
      }
    },
    "required": ["slots"]
  }
}
```
回傳的樣子
```json
{
  "role": "assistant",
  "function_call": {
    "name": "set_meeting_slots",
    "arguments": "{ 
      \"slots\": [
        [\"2025-04-11T09:00:00\", \"2025-04-11T10:00:00\"],
        [\"2025-04-11T13:00:00\", \"2025-04-11T14:00:00\"],
        [\"2025-04-11T16:00:00\", \"2025-04-11T17:00:00\"]
      ]
    }"
  }
}
```

## gpt_response
### 一般（非串流）成功回傳
gpt原始回傳的格式大概長這樣
所以才要用 `reply = response['choices'][0]`
choices：陣列是為了支援 n>1 時返回多個候選答案。
finish_reason：判斷回答是否被截斷（length）或因違規被過濾（content_filter）。
usage：用來計算成本；同樣欄位在 Streaming 也會於最後一包送回。
```json
{
  "id": "chatcmpl-abc1234567890",
  "object": "chat.completion",
  "created": 1713806400,          // UNIX 時戳
  "model": "gpt-4o-mina-2025-04-15",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "您好，這裡是回覆內容。",
        "tool_calls": null          // 使用 function/tool-calling 時才會出現
      },
      "finish_reason": "stop"       // 可能是 "stop" | "length" | "tool_calls" | "content_filter"
    }
  ],
  "usage": {                        // ← Input / Output token 在這裡
    "prompt_tokens": 45,
    "completion_tokens": 92,
    "total_tokens": 137
  },
  "system_fingerprint": "fp_a1b2c3..." // 選填：若開啟 system_fingerprint=true
}
```
### 串流（stream:true）模式（不會比較貴）
「stream : true」= 把完整回應切成一小塊一小塊（chunk）立即送出，而不是等全部字都生成完才一次回傳。
每行以 data: 開頭
delta 只包含新增的第一個文字；你需要把所有 chunk 依序串起來才是完整回答。
```json
event: message
data: {
  "id": "chatcmpl-abc1234567890",
  "object": "chat.completion.chunk",
  "created": 1713806401,
  "model": "gpt-4o-mina-2025-04-15",
  "choices": [
    {
      "index": 0,
      "delta": { "role": "assistant", "content": "您好" },
      "finish_reason": null
    }
  ]
}

event: message
data: {
  "id": "chatcmpl-abc1234567890",
  "object": "chat.completion.chunk",
  ...
  "choices": [
    {
      "index": 0,
      "delta": { "content": "，這裡是回覆內容。" },
      "finish_reason": "stop"
    }
  ]
}

event: done
data: {                             // 最終包帶 usage
  "usage": { "prompt_tokens": 45, "completion_tokens": 92, "total_tokens": 137 }
}
```
### Function / Tool Calling 回傳
當你在 messages 或 tools 內宣告函式定義，模型若決定呼叫函式，message 會改為下列結構：
```json
{
  "id": "chatcmpl-abc1234567890",
  "object": "chat.completion",
  "created": 1713806400,          // UNIX 時戳
  "model": "gpt-4o-mina-2025-04-15",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "tool_calls": [
          {
            "id": "call_01HE...",
            "type": "function",
            "function": {
              "name": "search_flights",
              "arguments": "{\"from\":\"TPE\",\"to\":\"KIX\",\"date\":\"2025-05-01\"}"
            }
          }
        ],
        "content": null
      },
      "finish_reason": "stop"       // 可能是 "stop" | "length" | "tool_calls" | "content_filter"
    }
  ],
  "usage": {                        // ← Input / Output token 在這裡
    "prompt_tokens": 45,
    "completion_tokens": 92,
    "total_tokens": 137
  },
  "system_fingerprint": "fp_a1b2c3..." // 選填：若開啟 system_fingerprint=true
}
```
### 錯誤回傳格式


type | code | 說明
| - | - | - |
invalid_request_error | context_length_exceeded | token 超過模型上限
insufficient_quota | insufficient_quota | 額度用完
rate_limit_error | rate_limit_exceeded | 達到 RPM / TPM 上限
server_error | 500 | 伺服器內部錯誤，可重試
```
{
  "error": {
    "message": "You exceeded your quota...",
    "type": "insufficient_quota",
    "param": null,
    "code": "insufficient_quota"
  }
}
```

## swift api 交互
定義的數據結構
```swift
struct OpenAIRequest: Codable {
    let model: String
    let messages: [OpenAIMessage]
    let temperature: Float
}

struct OpenAIResponse: Codable {
    let id: String
    let object: String
    let created: Int
    let model: String
    let choices: [OpenAIResponseChoice]
}
```
```swift
class ChatViewModel: ObservableObject {
    // 你的OpenAI API Key
    private let apiKey = "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"
    
    func sendMessageToGPT(messages: [ChatMessage]) async -> String? {
        // 將ChatMessage轉換為OpenAI格式 並且可以記住對話(是所有）
        let apiMessages = messages.map { message in
            OpenAIMessage(
                role: message.isMe ? "user" : "assistant",
                content: message.text
            )
        }
        
        let requestBody = OpenAIRequest(
            model: "gpt-4.1-mini",
            messages: apiMessages,
            temperature: 0.7
        )
        
        // 編碼（將 Swift 對象轉換為 JSON）：
        // 使用 JSONEncoder 將 OpenAIRequest 對象編碼成 JSON 數據
        // 這個 JSON 數據會被發送給 OpenAI API
        guard let requestData = try? JSONEncoder().encode(requestBody) else {
            print("Failed to encode request")
            return nil
        }
        
        guard let url = URL(string: "https://api.openai.com/v1/chat/completions") else {
            print("Invalid URL")
            return nil
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.addValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = requestData
        
        do {
            let (data, response) = try await URLSession.shared.data(for: request)
            
            guard let httpResponse = response as? HTTPURLResponse,
                  httpResponse.statusCode == 200 else {
                print("Error response: \(response)")
                if let errorString = String(data: data, encoding: .utf8) {
                    print("Response body: \(errorString)")
                }
                return nil
            }
            
            // 解碼（將 API 返回的 JSON 轉換回 Swift 對象）：
            // 使用 JSONDecoder 將 API 返回的 JSON 數據解碼成 OpenAIResponse 對象
            let openAIResponse = try JSONDecoder().decode(OpenAIResponse.self, from: data)
            if let choice = openAIResponse.choices.first {
                return choice.message.content
            } else {
                return nil
            }
        } catch {
            print("Network error: \(error)")
            return nil
        }
    }
}
```
這些結構體都遵循 Codable 協議，這使得它們可以自動進行 JSON 編碼和解碼。當數據在網絡上傳輸時：
發送請求時：Swift對象 → JSON數據
接收響應時：JSON數據 → Swift對象

**encode原理解釋**
apiMessages 會是一個 OpenAI 格式的消息數組，讓我用例子說明。
假設原始的 messages 數組包含以下對話：
```swift
[
    ChatMessage(text: "你好，我是小明", isMe: true),
    ChatMessage(text: "你好小明，很高興見到你", isMe: false),
    ChatMessage(text: "今天天氣真好", isMe: true)
]
```
經過 map 轉換後，apiMessages 會變成這樣的格式：
```swift
[
    OpenAIMessage(
        role: "user",           // isMe: true 所以是 user
        content: "你好，我是小明"
    ),
    OpenAIMessage(
        role: "assistant",      // isMe: false 所以是 assistant
        content: "你好小明，很高興見到你"
    ),
    OpenAIMessage(
        role: "user",           // isMe: true 所以是 user
        content: "今天天氣真好"
    )
]
```
當這個數據被編碼成 JSON 發送給 OpenAI API 時，會是這樣的格式：
```json
{
    "model": "gpt-4.1-mini",
    "messages": [
        {
            "role": "user",
            "content": "你好，我是小明"
        },
        {
            "role": "assistant",
            "content": "你好小明，很高興見到你"
        },
        {
            "role": "user",
            "content": "今天天氣真好"
        }
    ],
    "temperature": 0.7
}
```

# 專題紀錄
## 費用
### 測試結果
gpt4o-mini
線性代數-對角化的應用2024/12/10~2024/12/30, * 1000次
tokens用量：25000
費用：0.01美元（0.3台幣）

![截圖 2025-04-23 凌晨1.14.50](https://hackmd.io/_uploads/HkydOlLkxl.png)
![image](https://hackmd.io/_uploads/HJxY_eLyee.png)
![image](https://hackmd.io/_uploads/SkAR_g81xe.png)

### 省token的方法

| 手段 | 省 token 效果 | 備註 |
|------|---------------|------|
| **摘要舊訊息 / 只保留關鍵上下文** | ★★★★★ | 每少 1 K token ≈ 0.02 NTD（4o-mini input） |
| **改用 4o-mini 或動態模型升級** | ★★★★☆ | 價格差 6–10 倍；省下最明顯 |
| **降低溫度 / top-p，使回覆更短** | ★★★☆☆ | 若內容允許簡潔回應 |
| **JSON 最小化 / 刪空白** | ★★☆☆☆ | 規模大時才顯著 |
| **選擇標識縮寫（e.g. `"t"` 代 `"temperature"`）** | ★☆☆☆☆ | 只對巨量批次有感 |

* 先做「動態裁切 + 自動摘要」——投入一天，可立即砍掉 50 %以上 input token。
* 上 GPT-4o-mini 為預設，再寫條件判斷升級 GPT-4o。
* 研究 Embedding-RAG，把大綱或書本內容放向量庫，真正需要時才塞進 prompt。
* 若之後有「批次生成題庫」需求，再導入 Batch API 或快取。

## 資料結構
```swift
enum RepeatType: Hashable {
    case none
    case daily
    case weekly([Int])   // 0=Sunday, 1=Monday, ...
    case monthly([Int])  // 幾號
}
```
```swift
// Todo 任務資料結構
struct TodoTask: Identifiable {
    let id = UUID()                    // 唯一識別符
    var title: String                  // 任務標題
    var note: String                   // 任務備註
    var color: Color                   // 任務顏色
    var focusTime: Int                 // 專注時間（分鐘）
    var category: String               // 任務類別
    
    var isAllDay: Bool                 // 是否全天
    var isCompleted: Bool              // 是否完成
    var repeatType: RepeatType         // 重複類型
    var startDate: Date                // 任務開始日期（重複任務的起始日）
    var endDate: Date                  // 任務結束日期（重複任務的結束日）
    var createdAt: Date                // 任務建立時間
}
```
## procloudflare proxy 自架 (無firebase安全驗證)
### 限制時間
| 名稱 | 公式 & 來源 | 影響計費？ | 跟串流的關係 |
|------|-------------|-----------|--------------|
| **CPU time (Execution Duration)** | Worker 實際佔用 V8 執行緒的毫秒數 | **是**。免費方案每次最多 10 ms，Standard 預設 30 s，可加購到 300 s。| 只在你解析/轉送每個 chunk 時稍微累加；等待下個 chunk 的空檔不算 |
| **Request Duration (Wall-clock)** | 從請求進到節點開始，到最後一位元組送出為止 | 不計費；僅作為觀測指標。 | 串流會拉長這段時間，但不會拉高帳單 |
### 方案
![image](https://hackmd.io/_uploads/HJ6l9gfxxg.png)

#### 為何「串流」通常 更省 CPU time？
**一次回整段：**
Worker 先把 OpenAI 的整份回應讀完 → 暫存 → 再灌給前端。
單次 await apiRes.text() 會在 Worker 緩衝整段資料，CPU time 持續累加直到讀完。
**SSE 串流：**
Worker 把 ReadableStream 直接 pass-through；V8 只在每個 chunk 到來時醒一下，寫入 edge socket 後馬上 await。
多數時間處於 idle，不計 CPU。
實測同一段 1 500 字答案，串流版常見 CPU time < 5 ms，而「非串流 + resp.text()」可能 20-40 ms（仍在免費額度內，但數字高很多）。
### 教學
在專案資料夾內（例如 ~/Desktop/swift_app/cloudflare）
```bash
npm init -y
npm install wrangler # 瀏覽器跳出 OAuth
```
![image](https://hackmd.io/_uploads/HyhoLYUkex.png =300x)

```bash
npx wrangler login    #登入
npm create cloudflare@latest gpt-proxy-api
```
要選javascript 
最後部署deploy
![image](https://hackmd.io/_uploads/B1ZbZavkge.png)
```
cd gpt-proxy-api
```
**編輯wrangler.jsonc**
OPENAI_API_KEY 屬於 Secret，不能寫在檔案裡，下一步用指令加入。
```jsonc
{
	"name": "gpt-proxy-api",
	"main": "src/index.js",
	"compatibility_date": "2025-04-24",   // 建議寫今天日期
	"vars": {                             // 普通文字環境變數
		"API_TOKEN": "my-secret-token"
	}
}
```
**編輯src/index.js**
```javascript
export default {
	async fetch(request, env) {
	  // 1. 簡易白名單：驗證自訂 token（避免任何人呼叫）
	  if (request.headers.get("x-api-token") !== env.API_TOKEN) {
		return new Response("Unauthorized", { status: 401 });
	  }
  
	  // 2. 直接把 body 轉交給 OpenAI Chat Completions
	  const openaiResp = await fetch(
		"https://api.openai.com/v1/chat/completions",
		{
		  method: "POST",
		  headers: {
			"Authorization": `Bearer ${env.OPENAI_API_KEY}`,
			"Content-Type": "application/json"
		  },
		  body: request.body
		}
	  );
  
	  // 3. 將 OpenAI 的回應原樣丟回 App（status、headers、body 都保留）
	  return openaiResp;
	}
  };
```
加入秘密apikey
```bash
npx wrangler secret put OPENAI_API_KEY
```
大概長這樣type內是完整的`apikey`
```
[
  { "name": "OPENAI_API_KEY", "type": "sk-pro..." }
]
```
注意：這邊才輸入api的key
![截圖 2025-04-24 晚上10.35.46](https://hackmd.io/_uploads/SJ_Dnpv1ex.png)
```bash
#查看已經輸入的secret key
wrangler secret list
```
```bash
#正式部署
npx wrangler deploy
或是指定入口
npx wrangler deploy src/index.ts
```
成功新增 http... 那行等等 proxyURL = URL 會用到
![image](https://hackmd.io/_uploads/SJ3zqt8ygg.png)
在網頁確認
![image](https://hackmd.io/_uploads/HkHZ66D1ge.png =500x)
swift修改
```swift
class ChatViewModel: ObservableObject {
    // ❶ 刪掉存在 App 端的 OpenAI API Key
    // private let apiKey = "sk-..."  // ← 移除
    
    // ❷ 新增 Worker URL 與驗證 Token
    // ⬇︎ 修改
    private let proxyURL = URL(string: "https://gpt-proxy-api.<帳號>.workers.dev")!
    private let proxyToken = "my-secret-token"
    
    func sendMessageToGPT(messages: [ChatMessage]) async -> String? {
        ...
        // ⬇︎ 修改：改呼叫 proxyURL
        var request = URLRequest(url: proxyURL)
        request.httpMethod = "POST"
        // ⬇︎ 修改：使用自訂 header，**不再帶 OpenAI Key**
        request.addValue(proxyToken, forHTTPHeaderField: "x-api-token")
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = requestData
        ...
    }
}
```
### 安全問題
1. Cloudflare Worker 中 vars vs secret 差異：
**重點：無論 vars 還是 secret，瀏覽器端都看不到。**
Worker 是在 Cloudflare 邊緣執行的，程式碼（與環境變數）不會原封不動傳給用戶端。

類型 | 儲存方式 | 上線後能被誰看到？ | 適用情境
-| - | - | -
vars | 以純文字隨 Worker 一起部署 | 只有 Worker 執行環境（你的程式）能讀，使用者透過 HTTP 拿不到 | 不算機密，但也不想硬寫死在程式裡的常數
secret | 加密存放，Cloudflare 後端在執行時才解密注入 | 同上，只是連 Dashboard 也只顯示名稱，不顯示內容 | 真正機密——API Key、DB 密碼…
2. 不夠安全
**App 內要送 x-api-token**
你必須把 my-secret-token 硬寫在 App，或是從伺服器拿一次後緩存。
不論哪種，只要有人抓封包或逆向 App，就能抄到這組 token。
**Header 沒加密**
Token 會隨每次請求出現在 HTTP header。雖然傳輸層有 HTTPS，但任何能在客戶端植入惡意程式／裝置 SSL Proxy 的人，都能擷取。
**無身分綁定**
任何拿到這組 token 的人，只要知道 Worker URL，就能重播請求。

類型 | 儲存方式 | 上線後能被誰看到？ | 適用情境
-| - | - | -
vars | 以純文字隨 Worker 一起部署 | 只有 Worker 執行環境（你的程式）能讀，使用者透過 HTTP 拿不到 | 不算機密，但也不想硬寫死在程式裡的常數
secret | 加密存放，Cloudflare 後端在執行時才解密注入 | 同上，只是連 Dashboard 也只顯示名稱，不顯示內容 | 真正機密——API Key、DB 密碼…
### stream模式要用要用半雙工（但在cloudflare不用理會 因為是送到他們主機上游）
| 名稱 | 意義 | 與我們的串流聊天有何關聯 |
|------|------|--------------------------|
| **單工 (simplex)** | 先把整個 request 傳完，才能開始收 response；request 不能是 Stream | 傳統 `POST`；OpenAI 串流無法工作 |
| **半雙工 (half-duplex)** | 仍然**必須**把 request 全部送完，之後才能讀 response，但 request body 可以是 `ReadableStream` | ✅ 目前 fetch 唯一允許的模式；我們只要把小小的 JSON 送出去，就開始等 SSE |
|**全雙工 (full-duplex)**	|request 還沒傳完就能邊收 response、甚至雙向持續推資料	| 規格裡還在討論；瀏覽器與 CF Workers 都尚未實作|

OpenAI SSE 來說，我們只需要「上傳一個 JSON ⇒ 持續下載串流文字」，半雙工就夠了。

| 方向 | 你的 Worker 目前在做什麼 | 規格何時 **必須** `duplex:"half"` | 結論 |
|------|------------------------|-----------------------------------|-------|
| **上傳（request → OpenAI）** | 把一小段 **JSON**（`Uint8Array` / `ArrayBuffer`）一次送出去 | **只有**當 *request body* 是 `ReadableStream` 時才強制要宣告。<br>（標準稱為「可串流上傳」）|  依然合法。 |
| **下載（OpenAI → 前端）** | `openaiResp.body` 本來就是 `ReadableStream`，<br>你直接把整個 `Response` return 回去 | **下載端** 不需要 `duplex`；Cloudflare 會自動把 stream pass-through 給瀏覽器 | 因此 SSE 可以順利一路流到 SwiftUI。 |
```javascript
var index_default = {
  async fetch(request, env) {
    if (request.headers.get("x-api-token") !== env.API_TOKEN) {
      return new Response("Unauthorized", { status: 401 });
    }
    const openaiResp = await fetch(
      "https://api.openai.com/v1/chat/completions",
      {
        method: "POST",
        headers: {
          "Authorization": `Bearer ${env.OPENAI_API_KEY}`,
          "Content-Type": "application/json"
        },
        body: request.body
      }
    );
    return openaiResp;
  }
};
export {
  index_default as default
};
```


## 系統架構與安全
### App → 自家後端 → OpenAI
API Key 不落地客戶端。
後端可統一加上 RAG（向量搜尋）或商業邏輯。
屬於 「BFF—Backend For Frontend」 模式，符合 Apple 對遠端程式碼執行的規範。
### 向量資料庫
⚡ File Search（OpenAI 內建）雖然貴，但零維運；大規模時再換 Pinecone／Qdrant。
團隊小、用量可控的情況下，先用 File Search 省 DevOps 時間。
![image](https://hackmd.io/_uploads/rJr9OgIJgg.png)

### 內容過濾
把 moderations API 或 Assistants 策略模版串進 pipeline。
Apple 審核看重「違禁內容舉報機制」；這裡直接把 OpenAI 風險等級轉為前端遮罩。
### 資料保留策略
在隱私權政策列「對話最長保存 X 天，逾期自動刪除」。
支援 GDPR／台灣個資法的 刪除請求 API，可用背景排程定期清理。
### 提省錢三招
Batch API：離峰時段批量丟入，官方直接再折 50%（計畫內含）。
Cache hit：對重複提示用 cache=true；輸入、輸出各再省 75%。
分層路由：先用 nano 做「草稿」，再把需要高精度的少數請求升級到 GPT-4.1。

## 串流與call function大致流程
tool_calls 內容會分片（delta）回來：name 通常在第一包，arguments 可能拆成好幾包。
當 finish_reason:"tool_calls" 出現，就代表「這次函式參數已完整」— 該去執行了。
執行完把結果以 role:"tool" 附加到 messages，再發下一次請求；模型會讀取結果並接著回答。
```
(1) user prompt
      │
      ▼
(2) POST /chat/completions   ← 夾帶 tools 定義,  stream:true
      │
      ▼
(3) SSE chunk n=0            ← delta.role="assistant"
                              delta.tool_calls[0].function.name="search_flights"
      │
      │  …  （多個 chunk，arguments 逐字流回）
      ▼
(4) SSE chunk 最後一包        ← finish_reason="tool_calls"
      │
      ▼
(5) 伺服器執行你的函式        ↙ 把結果寫成 role:"tool" 再 append
      │
      ▼
(6) 再呼叫 /chat/completions 接續對話（可繼續串流）

```

### 1️⃣　第 1 次請求（含工具定義，stream:true）
```json
POST /v1/chat/completions
{
  "model": "gpt-4o",
  "stream": true,
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "search_flights",
        "description": "查機票",
        "parameters": {
          "type": "object",
          "properties": {
            "from": { "type": "string" },
            "to":   { "type": "string" },
            "date": { "type": "string" }
          },
          "required": ["from","to","date"]
        }
      }
    }
  ],
  "messages": [
    { "role": "system", "content": "你是一位旅遊助理" },
    { "role": "user",   "content": "幫我找 5/1 台北飛大阪的機票" }
  ]
}
```
### 2️⃣　串流回傳（逐塊示意）
```json
data: {                     // 第一塊：宣告要呼叫函式
  "choices":[{
    "delta":{
      "role":"assistant",
      "tool_calls":[{
        "id":"call_001",
        "type":"function",
        "function":{"name":"search_flights","arguments":"{"}
      }]
    }
  }]}
---
data: { "choices":[{ "delta":{"tool_calls":[{ "id":"call_001", "function":{"arguments":"\"from\":\"TPE\",\""}}] } }] }
...
data: { "choices":[{ "delta":{"tool_calls":[{ "id":"call_001", "function":{"arguments":"\"to\":\"KIX\",\"date\":\"2025-05-01\"}"} }] } }] }
---
data: { "choices":[{ "finish_reason":"tool_calls" }] }   ← 參數結束
你此時把 arguments 串成完整 JSON：

{"from":"TPE","to":"KIX","date":"2025-05-01"}
```
### 3️⃣　後端執行函式
```json
const result = await searchFlights({from:"TPE",to:"KIX",date:"2025-05-01"});
/* 假設回傳 ↓ */
{
  "flights":[
    {"airline":"JX","price":4200,"dep":"07:40","arr":"11:25"},
    {"airline":"CI","price":4500,"dep":"09:00","arr":"12:45"}
  ]
}
```
### 4️⃣　第 2 次請求（把結果加回對話）
```json
POST /v1/chat/completions
{
  "model": "gpt-4o",
  "stream": true,
  "messages": [
    { "role": "system", "content": "你是一位旅遊助理" },
    { "role": "user",   "content": "幫我找 5/1 台北飛大阪的機票" },
    {                   // GPT 剛才的 tool_call
      "role": "assistant",
      "tool_calls": [{
        "id": "call_001",
        "type": "function",
        "function": {
          "name": "search_flights",
          "arguments": "{\"from\":\"TPE\",\"to\":\"KIX\",\"date\":\"2025-05-01\"}"
        }
      }],
      "content": null
    },
    {                   // 你的程式塞進來的工具結果
      "role": "tool",
      "tool_call_id": "call_001",
      "content": "{\"flights\":[{\"airline\":\"JX\",...}]}"
    }
  ]
}
```
重要欄位
tool_call_id 必須對應前面 assistant 提供的 id。
content 放函式執行結果（JSON 或純文字皆可）。
所有先前訊息都要保留，讓模型維持上下文。​
模型收到這組新對話，就會把工具結果讀進記憶並生成最終回覆（仍可串流）：
```
「我找到了兩班直飛：JX 上午 7:40，票價 NT$4 200；CI 上午 9:00，票價 NT$4 500。需要我幫你預訂嗎？」
```


## 同一次stream中「先講話、後附帶 function call」

在串流過程中，模型可以先以 delta.content 實時吐出文字，等話說完後，再改吐 delta.function_call。這在社群討論中已被證實可行，常見輸出形態大概長這樣：
```
// 第一段
{ "delta": { "role": "assistant" } }
{ "delta": { "content": "好的，我來查詢……" } }

// 話說完後
{ "delta": { "content": null,
             "function_call": { "name": "getWeather" } } }

// 參數會繼續分段流出
{ "delta": { "function_call": { "arguments": "{\"city\":\"Tokyo\"}" } } }

// 最後
{ "finish_reason": "function_call" }
```
需要自己偵測 delta.function_call，並在 finish_reason=="function_call" 時觸發後端函式。
示範流程可參考論壇範例碼：
https://community.openai.com/t/functions-calling-with-streaming/305742
注意：OpenAI 規格定義「最終合併後的訊息」若含 function_call，content 應為 null；所以很多函式庫在串流結束時只保留 function_call 而把文字丟掉。你得在 串流時 就把文字收集起來顯示，別等到組裝完成才讀取。

## function定義
add 還有其他細節
```swift
// 保存任务的方法
private func saveTask() {
	// 创建新任务
	let newTask = TodoTask(
		title: title,
		note: content,
		color: selectedColor,
		focusTime: 0,  // 默认专注时间为0
		category: category,
		isAllDay: isAllDay,
		isCompleted: false,
		repeatType: repeat_option,
		startDate: startDate,
		endDate: endDate,
		createdAt: Date()  // 使用当前时间作为创建时间
	)
	// 将新任务添加到全域任务列表
	allTasks.tasks.append(newTask)
	// 新增後自動關閉新增視窗
	isPresented = false
}
```
todoview
```swift
// 儲存任務
    private func saveTask() {
        // 已經在保存中，避免重複觸發
        if viewModel.isLoading {
            return
        }
        
        // 驗證表單
        if !validateForm() {
            return
        }
        
        // 確保離線操作時 UI 不會卡住
        viewModel.isLoading = true
        
        Task {
            do {
                // 將重複選項資料傳遞給 viewModel
                viewModel.newTaskRepeatType = repeatOption
                
                // 獲取當前使用者 ID
                let userId = Auth.auth().currentUser?.uid ?? "default"
                
                // 創建任務，確保包含用戶 ID
                let task = TodoTask(
                    title: viewModel.newTaskTitle,
                    note: viewModel.newTaskNote,
                    color: viewModel.newTaskColor,
                    focusTime: viewModel.newTaskFocusTime,
                    category: viewModel.newTaskCategory,
                    isAllDay: viewModel.newTaskIsAllDay,
                    isCompleted: false,
                    repeatType: viewModel.newTaskRepeatType,
                    startDate: viewModel.newTaskStartDate,
                    endDate: viewModel.newTaskEndDate,
                    userId: userId
                )
                
                // 使用 addTask 而非 saveNewTask 以確保處理好 userId
                await viewModel.addTask(task)
                
                // 任務保存成功後關閉視圖
                dismissWithAnimation()
            } catch {
                // 顯示錯誤訊息
                viewModel.errorMessage = error.localizedDescription
                viewModel.isLoading = false
                print("Error saving task: \(error.localizedDescription)")
            }
        }
    }
```
todoview model
```swift
func addTask(_ task: TodoTask) async {
        isLoading = true
        errorMessage = nil
        
        // 確保已經登入
        guard let currentUserId = Auth.auth().currentUser?.uid else {
            errorMessage = "請先登入再新增任務"
            isLoading = false
            return
        }
        
        do {
            // 確保任務有使用者 ID
            var updatedTask = task
            updatedTask.userId = currentUserId
            
            try await firebaseService.saveTodoTask(updatedTask)
            
            // 重新加載任務列表以獲取最新數據
            try await loadTasks()
            
            // 發送資料變更通知
            postDataChangeNotification()
            
            // 清空錯誤訊息
            errorMessage = nil
        } catch {
            // 設置錯誤訊息
            errorMessage = "儲存任務失敗: \(error.localizedDescription)"
            print("Error adding task: \(error)")
        }
        
        isLoading = false
    }
```
firebase service
```swift
func saveTodoTask(_ task: TodoTask) async throws {
        currentSyncStatus = .syncing
        
        // 使用 Task 添加超時機制
        return try await withTimeout(seconds: 10) {
            do {
                let batch = self.db.batch()
                let userId = self.getCurrentUserId()
                
                // 創建一個包含當前使用者ID的任務副本
                var updatedTask = task
                updatedTask.userId = userId
                
                // 將任務存儲在使用者ID下
                let taskRef = self.db.collection(Collection.tasks.rawValue)
                                   .document(userId)
                                   .collection("userTasks")
                                   .document(task.id)
                
                // 儲存任務資料
                batch.setData(updatedTask.toFirestore, forDocument: taskRef)
                
                try await batch.commit()
                self.lastSync = Date()
                self.currentSyncStatus = .synced
            } catch {
                self.currentSyncStatus = .error(.networkError)
                print("Firebase error: \(error.localizedDescription)")
                throw error
            }
        }
}
```
# 其他
- [x] OpenAI thread 物件：建議至少保留與此 tool call 相關的對話，否則模型可能遺失上下文。若歷史太長，可裁剪、摘要或使用 OpenAI thread 物件（Assistants v2）來自動管理。
	**需要thread物件的情況：**
	=>
	**要跑 Code Interpreter：** 讓 GPT 幫你算數、畫圖、產生檔案。
	**要做 Retrieval / RAG：** 把 PDF、知識庫向量化後，讓 Assistant 自動引用。
	**多人共用同一案件：** 多個使用者同時追加訊息，伺服器端必須鎖定同一 conversation id。
	**想省掉手動裁切上下文、管理檔案：** Thread 幫你保存並「只抽必要段落」。
	**價錢：** 貴
	![image](https://hackmd.io/_uploads/rk-rzMWexl.png =400x)

	**（結論是目前只有聊天call function功能所以不用）**
- [x] Batch API：離峰時段批量丟入，官方直接再折 50%（計畫內含）。
	| 需求 | Batch API 優勢 | 同步 API 劣勢 |
	|------|----------------|---------------|
	| **大量離線內容生成**（摘要文件、翻譯全文、資料標註） | 一檔丟上去就好，OpenAI 幫你排程；**單價 5 折** | 你得自己做併發、流控、重試；單價原價 |
	| **不在乎秒級延遲**（可等數分鐘～數小時） | 背景作業，不佔用前端連線 | 同步模式要維持連線，易逾時 |
	| **想省錢** | 同模型直接省 50 % | — |
	
	「使用者按下送出要立即看到回答」或「需要串流漸進顯示」，Batch 就不適合。
- [x] Cache hit：對重複提示用 cache=true；輸入、輸出各再省 75%。
	**解釋：** 當使用者或程式發出請求時，所要的資料已經存在於「快取層」（記憶體、瀏覽器、CDN 邊緣節點、資料庫查詢快取…）裡，系統便直接從這一層取出並回應，不必再去「上游」或「原始來源」（磁碟、主資料庫、遠端伺服器）重抓或重算。這種情況就稱作 命中快取；反之，若快取裡沒有，便是 Cache miss（快取未命中）。
	**什麼時候「快取」才值得你加？**
	你打算內建教材或題庫檢索 → 同一題目、同一篇文章摘要重複度高。
	temperature 設 0、保證 determinism → 同 prompt 才能穩命中。
	批次離線作業 → 晚上排程跑，Hit 就回舊答案，Miss 再算。
	**如果以上都不是重點，就專心優化 prompt 長度和模型選擇，Cache Hit 可以先放一邊。**
- [x] Prompt-to-Response 快取
	把「同一段 Prompt → GPT 給出的完整回應」對映成一顆 Key–Value的hash table，先查快取，命中就直接回傳；沒命中才真正呼叫 OpenAI。能把「重複輸入」直接省下來，尤其在 文件翻譯、摘要、FAQ 等大量批次且高重複度的工作最有效。
	**自建 Prompt-to-Response 快取 —— 只在「固定問題」有價值**
	聊天場景裡，使用者輸入千奇百怪，幾乎不會重複；就算你把 prompt 標準化，命中率仍低於 5%。
	真正適合快取 的是：FAQ、固定範本翻譯、文件摘要……這些「同 prompt 重複率高、temperature = 0」的批次任務。
- [ ] 分層路由：先用 nano 做「草稿」，再把需要高精度的少數請求升級到 GPT-4.1。
- [x] openAI自己的向量資料庫（目前先不考慮）

- [x] gpt對話紀錄

### 前端 Streaming
- [ ] 使用 response_format: "json_object" 或者普通 text streaming；一旦偵測到 finish_reason=="function_call" 先停止渲染。
	執行函式 → 把結果塞回 messages → 重新 call GPT（或走 「工具結果」後續補串流 的模式）。
### UX 提示
- [ ] 在 UI 上顯示「GPT 正在更新你的行事曆⋯⋯」spinner，等 EventKit 執行完才解除。完成後，由 GPT 再說一句「已經幫你排好 ×××」；這句就來自 function 回傳後 GPT 的下一輪回答，流程一致。

- [ ] token 控制：回傳 JSON 前，用 JSONSerialization 壓成 string，再送給 GPT，避免不必要空白與縮排。（可能要總結的話會比較好）

- [x] temperature & top_p

### 容易出現的安全問題排名
- [ ] OWASP Top 10

### api key 不能寫在app裡面（安全問題）
- [x] 邊緣運算
	Cloudflare Worker
	Supabase Edge Function
	或是ai
	![image](https://hackmd.io/_uploads/SJ0cMu81xx.png)


### 登入問題
- [ ] apple goole 登入後的資料庫要怎麼知道

### firebase與sapabase比較
- [x] 結果應該還是用firebase
	![image](https://hackmd.io/_uploads/r1ZJGO8Jge.png)
	![image](https://hackmd.io/_uploads/r13WM_Iyeg.png)

### 免費ai
- [x] grog是免費的(但好像有使用量的問題)

### 日曆排序
- [ ] 日歷的陣列用排序的 然後再用lowerbound去找 找大於startdate 然後如果有repaet就不判斷有沒有小於enddate

### cloudflare串流時間
- [x] 看一下gpt的串流是否會降低cloudflare的時間 以及cloudflare有最多10秒的限制（免費方案）30秒（付費方案）


## 程式原理筆記
chatroom：在聊天室泡泡給使用者看的
allMessage：給gpt看的 每次使用者發訊息會從chatroom中載入 過程中append的function結果 不會被一直保存也不會被加到chatroom中

## 待更新改動

**問題**
- [x] 像是取的savetask資訊時聊天室不會顯示正在安排任務

- [x] 對話規則改為
	第一次對話使用 "none"
	第二次對話使用 "required"
	之後根據上一次的 tool_choice 和回覆類型來決定：
	- 如果上一次是 "required"，這次就用 "auto"
	- 如果上一次是 "auto"，則根據上一次回覆的類型決定：
		- 文字回覆用 "required"
		- 函數回覆用 "auto"
	一直重複到gpt使用endfunction

- [x] 聊天室列表要是逆排序 從最後一筆聊天記錄開始
- [x] gpt的save task聊天要可以選顏色
- [ ] 修正gpt問完問題會自己安排task的毛病
- [ ] 加上刪除功能
- [ ] 加上修改功能
- [ ] 測試要不要[wating user]
- [ ] 收到回應，狀態碼：429 (太頻繁回應)
- [ ] 刪除不一定會真的刪除
- [ ] 刪除匡的顯示資訊有問題
- [x] 新增語調設定
- [x] 讓gpt可以讀取到使用者習慣（把習慣時常調長）
- [x] 修正 gpt 不同比對話不會多換一行

今日改動：
新增重新傳送的機制解決錯誤代碼429
