AndyE7

Untitled

Aug 31st, 2026 (edited)
49
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 15.09 KB | None | 0 0
  1. -- ============================================================
  2. -- ChatGPT for CC:Tweaked
  3. -- GPT-5.6 Luna + Flex
  4. -- ============================================================
  5.  
  6. -- =========================
  7. -- CONFIG
  8. -- =========================
  9.  
  10. local API_KEY = "PUT_YOUR_OPENAI_API_KEY_HERE"
  11.  
  12. local MODEL = "gpt-5.6-luna"
  13. local SERVICE_TIER = "flex"
  14. local REASONING_EFFORT = "low"
  15.  
  16. local MAX_OUTPUT_TOKENS = 4096
  17.  
  18. -- CC:Tweaked HTTP timeout.
  19. -- Your version accepts a maximum of 60 seconds.
  20. local HTTP_TIMEOUT = 60
  21.  
  22. -- How often to check whether the background response is finished.
  23. local POLL_INTERVAL = 2
  24.  
  25. -- Overall time we will allow OpenAI to work on one response.
  26. -- This is NOT an HTTP timeout.
  27. local MAX_BACKGROUND_WAIT = 900
  28.  
  29. -- Retry initial submission if Flex temporarily has no capacity.
  30. local MAX_SUBMIT_RETRIES = 4
  31.  
  32. local API_URL = "https://api.openai.com/v1/responses"
  33.  
  34. local SYSTEM_PROMPT = [[
  35. You are ChatGPT running inside a Minecraft CC:Tweaked computer.
  36.  
  37. Answer clearly and concisely.
  38.  
  39. The user is interacting through a small text terminal.
  40. Prefer plain text over Markdown.
  41. Avoid tables unless genuinely useful.
  42. Keep formatting suitable for a narrow ComputerCraft terminal.
  43. ]]
  44.  
  45. -- =========================
  46. -- STATE
  47. -- =========================
  48.  
  49. local previousResponseId = nil
  50.  
  51. -- =========================
  52. -- COLOURS
  53. -- =========================
  54.  
  55. local function colour(c)
  56. if term.isColor() then
  57. term.setTextColor(c)
  58. end
  59. end
  60.  
  61. local function resetColour()
  62. if term.isColor() then
  63. term.setTextColor(colors.white)
  64. end
  65. end
  66.  
  67. -- =========================
  68. -- UI
  69. -- =========================
  70.  
  71. local function showCommands()
  72. colour(colors.gray)
  73.  
  74. print("Commands:")
  75. print(" /clear - start a new conversation")
  76. print(" /help - show commands")
  77. print(" /exit - quit")
  78. print("")
  79.  
  80. resetColour()
  81. end
  82.  
  83. local function clearScreen()
  84. term.clear()
  85. term.setCursorPos(1, 1)
  86. showCommands()
  87. end
  88.  
  89. -- =========================
  90. -- JSON HELPERS
  91. -- =========================
  92.  
  93. local function decodeJSON(body)
  94. if not body or body == "" then
  95. return nil
  96. end
  97.  
  98. local ok, data = pcall(
  99. textutils.unserializeJSON,
  100. body
  101. )
  102.  
  103. if ok then
  104. return data
  105. end
  106.  
  107. return nil
  108. end
  109.  
  110. local function extractError(body)
  111. if not body or body == "" then
  112. return nil
  113. end
  114.  
  115. local data = decodeJSON(body)
  116.  
  117. if data and data.error then
  118. if type(data.error) == "table" then
  119. return data.error.message
  120. or data.error.code
  121. or "Unknown OpenAI error"
  122. end
  123.  
  124. return tostring(data.error)
  125. end
  126.  
  127. return body
  128. end
  129.  
  130. local function extractOutputText(data)
  131. if not data then
  132. return nil
  133. end
  134.  
  135. -- Some API representations may expose this directly.
  136. if data.output_text
  137. and type(data.output_text) == "string"
  138. and data.output_text ~= "" then
  139.  
  140. return data.output_text
  141. end
  142.  
  143. local outputParts = {}
  144.  
  145. if data.output then
  146. for _, item in ipairs(data.output) do
  147.  
  148. if item.type == "message"
  149. and item.content then
  150.  
  151. for _, content in ipairs(item.content) do
  152.  
  153. if content.type == "output_text"
  154. and content.text then
  155.  
  156. table.insert(
  157. outputParts,
  158. content.text
  159. )
  160.  
  161. elseif content.type == "refusal"
  162. and content.refusal then
  163.  
  164. table.insert(
  165. outputParts,
  166. content.refusal
  167. )
  168. end
  169. end
  170. end
  171. end
  172. end
  173.  
  174. if #outputParts > 0 then
  175. return table.concat(
  176. outputParts,
  177. "\n"
  178. )
  179. end
  180.  
  181. return nil
  182. end
  183.  
  184. -- =========================
  185. -- HTTP HELPERS
  186. -- =========================
  187.  
  188. local function getErrorResponse(errorResponse)
  189. if not errorResponse then
  190. return nil, nil
  191. end
  192.  
  193. local code = nil
  194. local body = nil
  195.  
  196. pcall(function()
  197. code = errorResponse.getResponseCode()
  198. end)
  199.  
  200. pcall(function()
  201. body = errorResponse.readAll()
  202. end)
  203.  
  204. pcall(function()
  205. errorResponse.close()
  206. end)
  207.  
  208. return code, body
  209. end
  210.  
  211. local function authHeaders()
  212. return {
  213. ["Authorization"] = "Bearer " .. API_KEY,
  214. ["Content-Type"] = "application/json"
  215. }
  216. end
  217.  
  218. -- =========================
  219. -- CREATE BACKGROUND RESPONSE
  220. -- =========================
  221.  
  222. local function createResponse(userMessage)
  223. local payload = {
  224. model = MODEL,
  225.  
  226. input = userMessage,
  227.  
  228. instructions = SYSTEM_PROMPT,
  229.  
  230. service_tier = SERVICE_TIER,
  231.  
  232. reasoning = {
  233. effort = REASONING_EFFORT
  234. },
  235.  
  236. max_output_tokens = MAX_OUTPUT_TOKENS,
  237.  
  238. -- Critical for CC:Tweaked:
  239. -- OpenAI returns a response ID without requiring us
  240. -- to hold this HTTP connection open until generation finishes.
  241. background = true,
  242.  
  243. -- Required so we can retrieve it later and continue
  244. -- conversations with previous_response_id.
  245. store = true
  246. }
  247.  
  248. if previousResponseId then
  249. payload.previous_response_id =
  250. previousResponseId
  251. end
  252.  
  253. local body = textutils.serializeJSON(payload)
  254.  
  255. for attempt = 1, MAX_SUBMIT_RETRIES + 1 do
  256.  
  257. local ok,
  258. response,
  259. errorMessage,
  260. errorResponse = pcall(
  261. http.post,
  262. {
  263. url = API_URL,
  264. body = body,
  265. headers = authHeaders(),
  266. timeout = HTTP_TIMEOUT
  267. }
  268. )
  269.  
  270. -- CC itself threw an error.
  271. if not ok then
  272. return nil,
  273. "HTTP error: "
  274. .. tostring(response)
  275. end
  276.  
  277. -- Successful HTTP response.
  278. if response then
  279. local responseBody =
  280. response.readAll()
  281.  
  282. local responseCode =
  283. response.getResponseCode()
  284.  
  285. response.close()
  286.  
  287. local data =
  288. decodeJSON(responseBody)
  289.  
  290. if responseCode >= 200
  291. and responseCode < 300 then
  292.  
  293. if not data then
  294. return nil,
  295. "OpenAI returned invalid JSON."
  296. end
  297.  
  298. if not data.id then
  299. return nil,
  300. "OpenAI did not return a response ID."
  301. end
  302.  
  303. return data
  304. end
  305.  
  306. return nil,
  307. "HTTP "
  308. .. tostring(responseCode)
  309. .. ": "
  310. .. tostring(
  311. extractError(responseBody)
  312. or "Unknown error"
  313. )
  314. end
  315.  
  316. -- Failed HTTP request.
  317. local statusCode,
  318. errorBody =
  319. getErrorResponse(errorResponse)
  320.  
  321. local retryable =
  322. statusCode == 408
  323. or statusCode == 429
  324. or statusCode == 500
  325. or statusCode == 502
  326. or statusCode == 503
  327. or statusCode == 504
  328.  
  329. if retryable
  330. and attempt <= MAX_SUBMIT_RETRIES then
  331.  
  332. local delay =
  333. math.min(
  334. 2 ^ (attempt - 1),
  335. 16
  336. )
  337.  
  338. sleep(delay)
  339.  
  340. else
  341. local message =
  342. extractError(errorBody)
  343. or errorMessage
  344. or "Unknown HTTP error"
  345.  
  346. if statusCode then
  347. return nil,
  348. "HTTP "
  349. .. tostring(statusCode)
  350. .. ": "
  351. .. tostring(message)
  352. end
  353.  
  354. return nil,
  355. tostring(message)
  356. end
  357. end
  358.  
  359. return nil,
  360. "Could not submit request."
  361. end
  362.  
  363. -- =========================
  364. -- RETRIEVE RESPONSE
  365. -- =========================
  366.  
  367. local function retrieveResponse(responseId)
  368. local url =
  369. API_URL
  370. .. "/"
  371. .. responseId
  372.  
  373. local ok,
  374. response,
  375. errorMessage,
  376. errorResponse = pcall(
  377. http.get,
  378. {
  379. url = url,
  380. headers = authHeaders(),
  381. timeout = HTTP_TIMEOUT
  382. }
  383. )
  384.  
  385. if not ok then
  386. return nil,
  387. "HTTP error: "
  388. .. tostring(response)
  389. end
  390.  
  391. if response then
  392. local body =
  393. response.readAll()
  394.  
  395. local code =
  396. response.getResponseCode()
  397.  
  398. response.close()
  399.  
  400. if code >= 200
  401. and code < 300 then
  402.  
  403. local data =
  404. decodeJSON(body)
  405.  
  406. if not data then
  407. return nil,
  408. "OpenAI returned invalid JSON."
  409. end
  410.  
  411. return data
  412. end
  413.  
  414. return nil,
  415. "HTTP "
  416. .. tostring(code)
  417. .. ": "
  418. .. tostring(
  419. extractError(body)
  420. or "Unknown error"
  421. )
  422. end
  423.  
  424. local statusCode,
  425. errorBody =
  426. getErrorResponse(errorResponse)
  427.  
  428. local message =
  429. extractError(errorBody)
  430. or errorMessage
  431. or "Unknown HTTP error"
  432.  
  433. if statusCode then
  434. return nil,
  435. "HTTP "
  436. .. tostring(statusCode)
  437. .. ": "
  438. .. tostring(message)
  439. end
  440.  
  441. return nil,
  442. tostring(message)
  443. end
  444.  
  445. -- =========================
  446. -- WAIT FOR BACKGROUND RESPONSE
  447. -- =========================
  448.  
  449. local function waitForResponse(initialData)
  450. local responseId =
  451. initialData.id
  452.  
  453. -- It may occasionally finish immediately.
  454. if initialData.status == "completed" then
  455. return initialData
  456. end
  457.  
  458. local started =
  459. os.clock()
  460.  
  461. local consecutiveErrors = 0
  462.  
  463. while true do
  464.  
  465. if os.clock() - started
  466. >= MAX_BACKGROUND_WAIT then
  467.  
  468. return nil,
  469. "Response timed out after "
  470. .. tostring(MAX_BACKGROUND_WAIT)
  471. .. " seconds."
  472. end
  473.  
  474. sleep(POLL_INTERVAL)
  475.  
  476. local data, err =
  477. retrieveResponse(responseId)
  478.  
  479. if not data then
  480. consecutiveErrors =
  481. consecutiveErrors + 1
  482.  
  483. -- Allow temporary polling/network failures.
  484. if consecutiveErrors >= 5 then
  485. return nil, err
  486. end
  487. else
  488. consecutiveErrors = 0
  489.  
  490. local status =
  491. data.status
  492.  
  493. if status == "completed" then
  494. return data
  495.  
  496. elseif status == "failed" then
  497. local message = nil
  498.  
  499. if data.error then
  500. if type(data.error)
  501. == "table" then
  502.  
  503. message =
  504. data.error.message
  505. or data.error.code
  506. else
  507. message =
  508. tostring(data.error)
  509. end
  510. end
  511.  
  512. return nil,
  513. message
  514. or "OpenAI response failed."
  515.  
  516. elseif status == "cancelled" then
  517. return nil,
  518. "OpenAI response was cancelled."
  519.  
  520. elseif status == "incomplete" then
  521. local reason = nil
  522.  
  523. if data.incomplete_details then
  524. reason =
  525. data.incomplete_details.reason
  526. end
  527.  
  528. return nil,
  529. "Response incomplete"
  530. .. (
  531. reason
  532. and ": " .. tostring(reason)
  533. or "."
  534. )
  535.  
  536. elseif status == "queued"
  537. or status == "in_progress" then
  538.  
  539. -- Still running. Keep polling.
  540.  
  541. else
  542. return nil,
  543. "Unknown response status: "
  544. .. tostring(status)
  545. end
  546. end
  547. end
  548. end
  549.  
  550. -- =========================
  551. -- CHAT
  552. -- =========================
  553.  
  554. local function ask(userMessage)
  555. local initialData, createError =
  556. createResponse(userMessage)
  557.  
  558. if not initialData then
  559. return nil, createError
  560. end
  561.  
  562. local finalData, waitError =
  563. waitForResponse(initialData)
  564.  
  565. if not finalData then
  566. return nil, waitError
  567. end
  568.  
  569. local answer =
  570. extractOutputText(finalData)
  571.  
  572. if not answer then
  573. return nil,
  574. "OpenAI returned no text."
  575. end
  576.  
  577. -- Only advance conversation state once the response
  578. -- has successfully completed.
  579. previousResponseId =
  580. finalData.id
  581.  
  582. return answer
  583. end
  584.  
  585. -- =========================
  586. -- STARTUP
  587. -- =========================
  588.  
  589. if API_KEY == ""
  590. or API_KEY
  591. == "PUT_YOUR_OPENAI_API_KEY_HERE" then
  592.  
  593. colour(colors.red)
  594. print("Set API_KEY at the top of the script.")
  595. resetColour()
  596. return
  597. end
  598.  
  599. if not http then
  600. colour(colors.red)
  601. print("HTTP is disabled.")
  602. resetColour()
  603. return
  604. end
  605.  
  606. local allowed, reason =
  607. http.checkURL(API_URL)
  608.  
  609. if not allowed then
  610. colour(colors.red)
  611.  
  612. print("OpenAI API is blocked.")
  613. print(tostring(reason))
  614.  
  615. resetColour()
  616. return
  617. end
  618.  
  619. clearScreen()
  620.  
  621. -- =========================
  622. -- MAIN LOOP
  623. -- =========================
  624.  
  625. while true do
  626.  
  627. colour(colors.lime)
  628. write("You > ")
  629. resetColour()
  630.  
  631. local input =
  632. read()
  633.  
  634. if input == "/exit" then
  635. break
  636.  
  637. elseif input == "/clear" then
  638.  
  639. previousResponseId = nil
  640.  
  641. print("")
  642.  
  643. colour(colors.yellow)
  644. print("Conversation cleared.")
  645. resetColour()
  646.  
  647. print("")
  648.  
  649. elseif input == "/help" then
  650.  
  651. print("")
  652. showCommands()
  653.  
  654. elseif input ~= "" then
  655.  
  656. print("")
  657.  
  658. local _, thinkingY =
  659. term.getCursorPos()
  660.  
  661. colour(colors.gray)
  662. write("Thinking...")
  663. resetColour()
  664.  
  665. local answer, err =
  666. ask(input)
  667.  
  668. -- Remove Thinking... from the terminal.
  669. term.setCursorPos(
  670. 1,
  671. thinkingY
  672. )
  673.  
  674. term.clearLine()
  675.  
  676. if answer then
  677.  
  678. colour(colors.cyan)
  679. write("GPT > ")
  680. resetColour()
  681.  
  682. print(answer)
  683. print("")
  684.  
  685. else
  686.  
  687. colour(colors.red)
  688. print("Error: " .. tostring(err))
  689. resetColour()
  690.  
  691. print("")
  692. end
  693. end
  694. end
Advertisement
Add Comment
Please, Sign In to add comment